001/*- 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.interceptor.consent; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.api.Hook; 027import ca.uhn.fhir.interceptor.api.Interceptor; 028import ca.uhn.fhir.interceptor.api.Pointcut; 029import ca.uhn.fhir.rest.api.Constants; 030import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 031import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.api.server.ResponseDetails; 034import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 035import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters; 036import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 037import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; 038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 039import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationConstants; 040import ca.uhn.fhir.rest.server.util.ICachedSearchDetails; 041import ca.uhn.fhir.util.BundleUtil; 042import ca.uhn.fhir.util.IModelVisitor2; 043import jakarta.annotation.Nonnull; 044import jakarta.annotation.Nullable; 045import org.apache.commons.lang3.Validate; 046import org.hl7.fhir.instance.model.api.IBase; 047import org.hl7.fhir.instance.model.api.IBaseBundle; 048import org.hl7.fhir.instance.model.api.IBaseExtension; 049import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 050import org.hl7.fhir.instance.model.api.IBaseResource; 051 052import java.util.ArrayList; 053import java.util.Arrays; 054import java.util.Collections; 055import java.util.IdentityHashMap; 056import java.util.List; 057import java.util.Map; 058import java.util.concurrent.atomic.AtomicInteger; 059import java.util.stream.Collectors; 060 061import static ca.uhn.fhir.rest.api.Constants.URL_TOKEN_METADATA; 062import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_META; 063 064/** 065 * The ConsentInterceptor can be used to apply arbitrary consent rules and data access policies 066 * on responses from a FHIR server. 067 * <p> 068 * See <a href="https://hapifhir.io/hapi-fhir/docs/security/consent_interceptor.html">Consent Interceptor</a> for 069 * more information on this interceptor. 070 * </p> 071 */ 072@Interceptor(order = AuthorizationConstants.ORDER_CONSENT_INTERCEPTOR) 073public class ConsentInterceptor { 074 private static final AtomicInteger ourInstanceCount = new AtomicInteger(0); 075 private final int myInstanceIndex = ourInstanceCount.incrementAndGet(); 076 private final String myRequestAuthorizedKey = 077 ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_AUTHORIZED"; 078 private final String myRequestCompletedKey = 079 ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_COMPLETED"; 080 private final String myRequestSeenResourcesKey = 081 ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES"; 082 083 private volatile List<IConsentService> myConsentService = Collections.emptyList(); 084 private IConsentContextServices myContextConsentServices = IConsentContextServices.NULL_IMPL; 085 086 /** 087 * Constructor 088 */ 089 public ConsentInterceptor() { 090 super(); 091 } 092 093 /** 094 * Constructor 095 * 096 * @param theConsentService Must not be <code>null</code> 097 */ 098 public ConsentInterceptor(IConsentService theConsentService) { 099 this(theConsentService, IConsentContextServices.NULL_IMPL); 100 } 101 102 /** 103 * Constructor 104 * 105 * @param theConsentService Must not be <code>null</code> 106 * @param theContextConsentServices Must not be <code>null</code> 107 */ 108 public ConsentInterceptor(IConsentService theConsentService, IConsentContextServices theContextConsentServices) { 109 setConsentService(theConsentService); 110 setContextConsentServices(theContextConsentServices); 111 } 112 113 public void setContextConsentServices(IConsentContextServices theContextConsentServices) { 114 Validate.notNull(theContextConsentServices, "theContextConsentServices must not be null"); 115 myContextConsentServices = theContextConsentServices; 116 } 117 118 /** 119 * @deprecated Use {@link #registerConsentService(IConsentService)} instead 120 */ 121 @Deprecated 122 public void setConsentService(IConsentService theConsentService) { 123 Validate.notNull(theConsentService, "theConsentService must not be null"); 124 myConsentService = Collections.singletonList(theConsentService); 125 } 126 127 /** 128 * Adds a consent service to the chain. 129 * <p> 130 * Thread safety note: This method can be called while the service is actively processing requestes 131 * 132 * @param theConsentService The service to register. Must not be <code>null</code>. 133 * @since 6.0.0 134 */ 135 public ConsentInterceptor registerConsentService(IConsentService theConsentService) { 136 Validate.notNull(theConsentService, "theConsentService must not be null"); 137 List<IConsentService> newList = new ArrayList<>(myConsentService.size() + 1); 138 newList.addAll(myConsentService); 139 newList.add(theConsentService); 140 myConsentService = newList; 141 return this; 142 } 143 144 /** 145 * Removes a consent service from the chain. 146 * <p> 147 * Thread safety note: This method can be called while the service is actively processing requestes 148 * 149 * @param theConsentService The service to unregister. Must not be <code>null</code>. 150 * @since 6.0.0 151 */ 152 public ConsentInterceptor unregisterConsentService(IConsentService theConsentService) { 153 Validate.notNull(theConsentService, "theConsentService must not be null"); 154 List<IConsentService> newList = 155 myConsentService.stream().filter(t -> t != theConsentService).collect(Collectors.toList()); 156 myConsentService = newList; 157 return this; 158 } 159 160 @Hook(value = Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED) 161 public void interceptPreHandled(RequestDetails theRequestDetails) { 162 if (isSkipServiceForRequest(theRequestDetails)) { 163 return; 164 } 165 166 validateParameter(theRequestDetails.getParameters()); 167 168 for (IConsentService nextService : myConsentService) { 169 ConsentOutcome outcome = nextService.startOperation(theRequestDetails, myContextConsentServices); 170 Validate.notNull(outcome, "Consent service returned null outcome"); 171 172 switch (outcome.getStatus()) { 173 case REJECT: 174 throw toForbiddenOperationException(outcome); 175 case PROCEED: 176 continue; 177 case AUTHORIZED: 178 Map<Object, Object> userData = theRequestDetails.getUserData(); 179 userData.put(myRequestAuthorizedKey, Boolean.TRUE); 180 return; 181 } 182 } 183 } 184 185 /** 186 * Check if this request is eligible for cached search results. 187 * We can't use a cached result if consent may use canSeeResource. 188 * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource() 189 * to see if this holds. 190 * @return may the request be satisfied from cache. 191 */ 192 @Hook(value = Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH) 193 public boolean interceptPreCheckForCachedSearch(@Nonnull RequestDetails theRequestDetails) { 194 return !isProcessCanSeeResource(theRequestDetails, null); 195 } 196 197 /** 198 * Check if the search results from this request might be reused by later searches. 199 * We can't use a cached result if consent may use canSeeResource. 200 * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource() 201 * to see if this holds. 202 * If not, marks the result as single-use. 203 */ 204 @Hook(value = Pointcut.STORAGE_PRESEARCH_REGISTERED) 205 public void interceptPreSearchRegistered( 206 RequestDetails theRequestDetails, ICachedSearchDetails theCachedSearchDetails) { 207 if (isProcessCanSeeResource(theRequestDetails, null)) { 208 theCachedSearchDetails.setCannotBeReused(); 209 } 210 } 211 212 @Hook(value = Pointcut.STORAGE_PREACCESS_RESOURCES) 213 public void interceptPreAccess( 214 RequestDetails theRequestDetails, IPreResourceAccessDetails thePreResourceAccessDetails) { 215 216 // Flags for each service 217 boolean[] processConsentSvcs = new boolean[myConsentService.size()]; 218 boolean processAnyConsentSvcs = isProcessCanSeeResource(theRequestDetails, processConsentSvcs); 219 220 if (!processAnyConsentSvcs) { 221 return; 222 } 223 224 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 225 getAlreadySeenResourcesMap(theRequestDetails); 226 for (int resourceIdx = 0; resourceIdx < thePreResourceAccessDetails.size(); resourceIdx++) { 227 IBaseResource nextResource = thePreResourceAccessDetails.getResource(resourceIdx); 228 for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) { 229 IConsentService nextService = myConsentService.get(consentSvcIdx); 230 231 if (!processConsentSvcs[consentSvcIdx]) { 232 continue; 233 } 234 235 ConsentOutcome outcome = 236 nextService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices); 237 Validate.notNull(outcome, "Consent service returned null outcome"); 238 Validate.isTrue( 239 outcome.getResource() == null, 240 "Consent service returned a resource in its outcome. This is not permitted in canSeeResource(..)"); 241 242 boolean skipSubsequentServices = false; 243 switch (outcome.getStatus()) { 244 case PROCEED: 245 break; 246 case AUTHORIZED: 247 alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.AUTHORIZED); 248 skipSubsequentServices = true; 249 break; 250 case REJECT: 251 alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.REJECT); 252 thePreResourceAccessDetails.setDontReturnResourceAtIndex(resourceIdx); 253 skipSubsequentServices = true; 254 break; 255 } 256 257 if (skipSubsequentServices) { 258 break; 259 } 260 } 261 } 262 } 263 264 /** 265 * Is canSeeResource() active in any services? 266 * @param theProcessConsentSvcsFlags filled in with the responses from shouldProcessCanSeeResource each service 267 * @return true of any service responded true to shouldProcessCanSeeResource() 268 */ 269 private boolean isProcessCanSeeResource( 270 @Nonnull RequestDetails theRequestDetails, @Nullable boolean[] theProcessConsentSvcsFlags) { 271 if (isRequestAuthorized(theRequestDetails)) { 272 return false; 273 } 274 if (isSkipServiceForRequest(theRequestDetails)) { 275 return false; 276 } 277 if (myConsentService.isEmpty()) { 278 return false; 279 } 280 281 if (theProcessConsentSvcsFlags == null) { 282 theProcessConsentSvcsFlags = new boolean[myConsentService.size()]; 283 } 284 Validate.isTrue(theProcessConsentSvcsFlags.length == myConsentService.size()); 285 boolean processAnyConsentSvcs = false; 286 for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) { 287 IConsentService nextService = myConsentService.get(consentSvcIdx); 288 289 boolean shouldCallCanSeeResource = 290 nextService.shouldProcessCanSeeResource(theRequestDetails, myContextConsentServices); 291 processAnyConsentSvcs |= shouldCallCanSeeResource; 292 theProcessConsentSvcsFlags[consentSvcIdx] = shouldCallCanSeeResource; 293 } 294 return processAnyConsentSvcs; 295 } 296 297 @Hook(value = Pointcut.STORAGE_PRESHOW_RESOURCES) 298 public void interceptPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails thePreResourceShowDetails) { 299 if (isRequestAuthorized(theRequestDetails)) { 300 return; 301 } 302 if (isAllowListedRequest(theRequestDetails)) { 303 return; 304 } 305 if (isSkipServiceForRequest(theRequestDetails)) { 306 return; 307 } 308 if (myConsentService.isEmpty()) { 309 return; 310 } 311 312 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 313 getAlreadySeenResourcesMap(theRequestDetails); 314 315 for (int i = 0; i < thePreResourceShowDetails.size(); i++) { 316 317 IBaseResource resource = thePreResourceShowDetails.getResource(i); 318 if (resource == null 319 || alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) { 320 continue; 321 } 322 323 for (IConsentService nextService : myConsentService) { 324 ConsentOutcome nextOutcome = 325 nextService.willSeeResource(theRequestDetails, resource, myContextConsentServices); 326 IBaseResource newResource = nextOutcome.getResource(); 327 328 switch (nextOutcome.getStatus()) { 329 case PROCEED: 330 if (newResource != null) { 331 thePreResourceShowDetails.setResource(i, newResource); 332 resource = newResource; 333 } 334 continue; 335 case AUTHORIZED: 336 alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED); 337 if (newResource != null) { 338 thePreResourceShowDetails.setResource(i, newResource); 339 } 340 continue; 341 case REJECT: 342 alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT); 343 if (nextOutcome.getOperationOutcome() != null) { 344 IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome(); 345 thePreResourceShowDetails.setResource(i, newOperationOutcome); 346 alreadySeenResources.put(newOperationOutcome, ConsentOperationStatusEnum.PROCEED); 347 } else { 348 resource = null; 349 thePreResourceShowDetails.setResource(i, null); 350 } 351 continue; 352 } 353 } 354 } 355 } 356 357 @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE) 358 public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails) { 359 if (theResponseDetails.getResponseResource() == null) { 360 return; 361 } 362 if (isRequestAuthorized(theRequestDetails)) { 363 return; 364 } 365 if (isAllowListedRequest(theRequestDetails)) { 366 return; 367 } 368 if (isSkipServiceForRequest(theRequestDetails)) { 369 return; 370 } 371 if (myConsentService.isEmpty()) { 372 return; 373 } 374 375 // Take care of outer resource first 376 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 377 getAlreadySeenResourcesMap(theRequestDetails); 378 if (alreadySeenResources.containsKey(theResponseDetails.getResponseResource())) { 379 // we've already seen this resource before 380 ConsentOperationStatusEnum decisionOnResource = 381 alreadySeenResources.get(theResponseDetails.getResponseResource()); 382 383 if (ConsentOperationStatusEnum.AUTHORIZED.equals(decisionOnResource) 384 || ConsentOperationStatusEnum.REJECT.equals(decisionOnResource)) { 385 // the consent service decision on the resource was AUTHORIZED or REJECT. 386 // In both cases, we can immediately return without checking children 387 return; 388 } 389 } else { 390 // we haven't seen this resource before 391 // mark it as seen now, set the initial consent decision value to PROCEED by default, 392 // we will update if it changes another value below 393 alreadySeenResources.put(theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.PROCEED); 394 395 for (IConsentService next : myConsentService) { 396 final ConsentOutcome outcome = next.willSeeResource( 397 theRequestDetails, theResponseDetails.getResponseResource(), myContextConsentServices); 398 if (outcome.getResource() != null) { 399 theResponseDetails.setResponseResource(outcome.getResource()); 400 } 401 402 // Clear the total 403 if (theResponseDetails.getResponseResource() instanceof IBaseBundle) { 404 BundleUtil.setTotal( 405 theRequestDetails.getFhirContext(), 406 (IBaseBundle) theResponseDetails.getResponseResource(), 407 null); 408 } 409 410 switch (outcome.getStatus()) { 411 case REJECT: 412 alreadySeenResources.put( 413 theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.REJECT); 414 if (outcome.getOperationOutcome() != null) { 415 theResponseDetails.setResponseResource(outcome.getOperationOutcome()); 416 } else { 417 theResponseDetails.setResponseResource(null); 418 theResponseDetails.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT); 419 } 420 // Return immediately 421 return; 422 case AUTHORIZED: 423 alreadySeenResources.put( 424 theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.AUTHORIZED); 425 // Don't check children, so return immediately 426 return; 427 case PROCEED: 428 // Check children, so proceed 429 break; 430 } 431 } 432 } 433 434 // See child resources 435 IBaseResource outerResource = theResponseDetails.getResponseResource(); 436 FhirContext ctx = theRequestDetails.getServer().getFhirContext(); 437 IModelVisitor2 visitor = new IModelVisitor2() { 438 @Override 439 public boolean acceptElement( 440 IBase theElement, 441 List<IBase> theContainingElementPath, 442 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 443 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 444 445 // Clear the total 446 if (theElement instanceof IBaseBundle) { 447 BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theElement, null); 448 } 449 450 if (theElement == outerResource) { 451 return true; 452 } 453 if (theElement instanceof IBaseResource) { 454 IBaseResource resource = (IBaseResource) theElement; 455 if (alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) { 456 return true; 457 } 458 459 boolean shouldCheckChildren = true; 460 for (IConsentService next : myConsentService) { 461 ConsentOutcome childOutcome = 462 next.willSeeResource(theRequestDetails, resource, myContextConsentServices); 463 464 IBaseResource replacementResource = null; 465 boolean shouldReplaceResource = false; 466 467 switch (childOutcome.getStatus()) { 468 case REJECT: 469 replacementResource = childOutcome.getOperationOutcome(); 470 shouldReplaceResource = true; 471 alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT); 472 break; 473 case PROCEED: 474 replacementResource = childOutcome.getResource(); 475 shouldReplaceResource = replacementResource != null; 476 break; 477 case AUTHORIZED: 478 replacementResource = childOutcome.getResource(); 479 shouldReplaceResource = replacementResource != null; 480 shouldCheckChildren = false; 481 alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED); 482 break; 483 } 484 485 if (shouldReplaceResource) { 486 IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2); 487 BaseRuntimeChildDefinition containerChildElement = 488 theChildDefinitionPath.get(theChildDefinitionPath.size() - 1); 489 containerChildElement.getMutator().setValue(container, replacementResource); 490 resource = replacementResource; 491 } 492 } 493 494 return shouldCheckChildren; 495 } 496 497 return true; 498 } 499 500 @Override 501 public boolean acceptUndeclaredExtension( 502 IBaseExtension<?, ?> theNextExt, 503 List<IBase> theContainingElementPath, 504 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 505 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 506 return true; 507 } 508 }; 509 ctx.newTerser().visit(outerResource, visitor); 510 } 511 512 @Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION) 513 public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) { 514 theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE); 515 for (IConsentService next : myConsentService) { 516 next.completeOperationFailure(theRequest, theException, myContextConsentServices); 517 } 518 } 519 520 @Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY) 521 public void requestSucceeded(RequestDetails theRequest) { 522 if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) { 523 return; 524 } 525 for (IConsentService next : myConsentService) { 526 next.completeOperationSuccess(theRequest, myContextConsentServices); 527 } 528 } 529 530 protected RequestDetails getRequestDetailsForCurrentExportOperation( 531 BulkExportJobParameters theParameters, IBaseResource theBaseResource) { 532 // bulk exports are system operations 533 SystemRequestDetails details = new SystemRequestDetails(); 534 return details; 535 } 536 537 @Hook(value = Pointcut.STORAGE_BULK_EXPORT_RESOURCE_INCLUSION) 538 public boolean shouldBulkExportIncludeResource(BulkExportJobParameters theParameters, IBaseResource theResource) { 539 RequestDetails requestDetails = getRequestDetailsForCurrentExportOperation(theParameters, theResource); 540 541 for (IConsentService next : myConsentService) { 542 ConsentOutcome nextOutcome = next.willSeeResource(requestDetails, theResource, myContextConsentServices); 543 544 ConsentOperationStatusEnum status = nextOutcome.getStatus(); 545 switch (status) { 546 case AUTHORIZED: 547 case PROCEED: 548 // go to the next 549 break; 550 case REJECT: 551 // if any consent service rejects, 552 // reject the resource 553 return false; 554 } 555 } 556 557 // default is to include the resource 558 return true; 559 } 560 561 private boolean isRequestAuthorized(RequestDetails theRequestDetails) { 562 boolean retVal = false; 563 if (theRequestDetails != null) { 564 Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey); 565 retVal = Boolean.TRUE.equals(authorizedObj); 566 } 567 return retVal; 568 } 569 570 private boolean isSkipServiceForRequest(RequestDetails theRequestDetails) { 571 return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails); 572 } 573 574 private boolean isAllowListedRequest(RequestDetails theRequestDetails) { 575 return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails); 576 } 577 578 private boolean isMetaOperation(RequestDetails theRequestDetails) { 579 return theRequestDetails != null && OPERATION_META.equals(theRequestDetails.getOperation()); 580 } 581 582 private boolean isMetadataPath(RequestDetails theRequestDetails) { 583 return theRequestDetails != null && URL_TOKEN_METADATA.equals(theRequestDetails.getRequestPath()); 584 } 585 586 private void validateParameter(Map<String, String[]> theParameterMap) { 587 if (theParameterMap != null) { 588 if (theParameterMap.containsKey(Constants.PARAM_SEARCH_TOTAL_MODE) 589 && Arrays.stream(theParameterMap.get("_total")).anyMatch("accurate"::equals)) { 590 throw new InvalidRequestException(Msg.code(2037) + Constants.PARAM_SEARCH_TOTAL_MODE 591 + "=accurate is not permitted on this server"); 592 } 593 if (theParameterMap.containsKey(Constants.PARAM_SUMMARY) 594 && Arrays.stream(theParameterMap.get("_summary")).anyMatch("count"::equals)) { 595 throw new InvalidRequestException( 596 Msg.code(2038) + Constants.PARAM_SUMMARY + "=count is not permitted on this server"); 597 } 598 } 599 } 600 601 /** 602 * The map returned by this method keeps track of the resources already processed by ConsentInterceptor in the 603 * context of a request. 604 * If the map contains a particular resource, it means that the resource has already been processed and the value 605 * is the status returned by consent services for that resource. 606 * @param theRequestDetails 607 * @return 608 */ 609 @SuppressWarnings("unchecked") 610 private IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> getAlreadySeenResourcesMap( 611 RequestDetails theRequestDetails) { 612 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 613 (IdentityHashMap<IBaseResource, ConsentOperationStatusEnum>) 614 theRequestDetails.getUserData().get(myRequestSeenResourcesKey); 615 if (alreadySeenResources == null) { 616 alreadySeenResources = new IdentityHashMap<>(); 617 theRequestDetails.getUserData().put(myRequestSeenResourcesKey, alreadySeenResources); 618 } 619 return alreadySeenResources; 620 } 621 622 private static ForbiddenOperationException toForbiddenOperationException(ConsentOutcome theOutcome) { 623 IBaseOperationOutcome operationOutcome = null; 624 if (theOutcome.getOperationOutcome() != null) { 625 operationOutcome = theOutcome.getOperationOutcome(); 626 } 627 return new ForbiddenOperationException("Rejected by consent service", operationOutcome); 628 } 629}