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