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