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 Map<Object, Object> userData = theRequestDetails.getUserData(); 185 userData.put(myRequestAuthorizedKey, Boolean.TRUE); 186 return; 187 } 188 } 189 } 190 191 /** 192 * Check if this request is eligible for cached search results. 193 * We can't use a cached result if consent may use canSeeResource. 194 * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource() 195 * to see if this holds. 196 * @return may the request be satisfied from cache. 197 */ 198 @Hook(value = Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH) 199 public boolean interceptPreCheckForCachedSearch(@Nonnull RequestDetails theRequestDetails) { 200 return !isProcessCanSeeResource(theRequestDetails, null); 201 } 202 203 /** 204 * Check if the search results from this request might be reused by later searches. 205 * We can't use a cached result if consent may use canSeeResource. 206 * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource() 207 * to see if this holds. 208 * If not, marks the result as single-use. 209 */ 210 @Hook(value = Pointcut.STORAGE_PRESEARCH_REGISTERED) 211 public void interceptPreSearchRegistered( 212 RequestDetails theRequestDetails, ICachedSearchDetails theCachedSearchDetails) { 213 if (isProcessCanSeeResource(theRequestDetails, null)) { 214 theCachedSearchDetails.setCannotBeReused(); 215 } 216 } 217 218 @Hook(value = Pointcut.STORAGE_PREACCESS_RESOURCES) 219 public void interceptPreAccess( 220 RequestDetails theRequestDetails, IPreResourceAccessDetails thePreResourceAccessDetails) { 221 222 // Flags for each service 223 boolean[] processConsentSvcs = new boolean[myConsentService.size()]; 224 boolean processAnyConsentSvcs = isProcessCanSeeResource(theRequestDetails, processConsentSvcs); 225 226 if (!processAnyConsentSvcs) { 227 return; 228 } 229 230 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 231 getAlreadySeenResourcesMap(theRequestDetails); 232 for (int resourceIdx = 0; resourceIdx < thePreResourceAccessDetails.size(); resourceIdx++) { 233 IBaseResource nextResource = thePreResourceAccessDetails.getResource(resourceIdx); 234 for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) { 235 IConsentService nextService = myConsentService.get(consentSvcIdx); 236 237 if (!processConsentSvcs[consentSvcIdx]) { 238 continue; 239 } 240 241 ConsentOutcome outcome = 242 nextService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices); 243 Validate.notNull(outcome, "Consent service returned null outcome"); 244 Validate.isTrue( 245 outcome.getResource() == null, 246 "Consent service returned a resource in its outcome. This is not permitted in canSeeResource(..)"); 247 248 boolean skipSubsequentServices = false; 249 switch (outcome.getStatus()) { 250 case PROCEED: 251 break; 252 case AUTHORIZED: 253 alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.AUTHORIZED); 254 skipSubsequentServices = true; 255 break; 256 case REJECT: 257 alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.REJECT); 258 thePreResourceAccessDetails.setDontReturnResourceAtIndex(resourceIdx); 259 skipSubsequentServices = true; 260 break; 261 } 262 263 if (skipSubsequentServices) { 264 break; 265 } 266 } 267 } 268 } 269 270 /** 271 * Is canSeeResource() active in any services? 272 * @param theProcessConsentSvcsFlags filled in with the responses from shouldProcessCanSeeResource each service 273 * @return true of any service responded true to shouldProcessCanSeeResource() 274 */ 275 private boolean isProcessCanSeeResource( 276 @Nonnull RequestDetails theRequestDetails, @Nullable boolean[] theProcessConsentSvcsFlags) { 277 if (isRequestAuthorized(theRequestDetails)) { 278 return false; 279 } 280 if (isSkipServiceForRequest(theRequestDetails)) { 281 return false; 282 } 283 if (myConsentService.isEmpty()) { 284 return false; 285 } 286 287 if (theProcessConsentSvcsFlags == null) { 288 theProcessConsentSvcsFlags = new boolean[myConsentService.size()]; 289 } 290 Validate.isTrue(theProcessConsentSvcsFlags.length == myConsentService.size()); 291 boolean processAnyConsentSvcs = false; 292 for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) { 293 IConsentService nextService = myConsentService.get(consentSvcIdx); 294 295 boolean shouldCallCanSeeResource = 296 nextService.shouldProcessCanSeeResource(theRequestDetails, myContextConsentServices); 297 processAnyConsentSvcs |= shouldCallCanSeeResource; 298 theProcessConsentSvcsFlags[consentSvcIdx] = shouldCallCanSeeResource; 299 } 300 return processAnyConsentSvcs; 301 } 302 303 @Hook(value = Pointcut.STORAGE_PRESHOW_RESOURCES) 304 public void interceptPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails thePreResourceShowDetails) { 305 if (isRequestAuthorized(theRequestDetails)) { 306 return; 307 } 308 if (isAllowListedRequest(theRequestDetails)) { 309 return; 310 } 311 if (isSkipServiceForRequest(theRequestDetails)) { 312 return; 313 } 314 if (myConsentService.isEmpty()) { 315 return; 316 } 317 318 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 319 getAlreadySeenResourcesMap(theRequestDetails); 320 321 for (int i = 0; i < thePreResourceShowDetails.size(); i++) { 322 323 IBaseResource resource = thePreResourceShowDetails.getResource(i); 324 if (resource == null 325 || alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) { 326 continue; 327 } 328 329 for (IConsentService nextService : myConsentService) { 330 ConsentOutcome nextOutcome = 331 nextService.willSeeResource(theRequestDetails, resource, myContextConsentServices); 332 IBaseResource newResource = nextOutcome.getResource(); 333 334 switch (nextOutcome.getStatus()) { 335 case PROCEED: 336 if (newResource != null) { 337 thePreResourceShowDetails.setResource(i, newResource); 338 resource = newResource; 339 } 340 continue; 341 case AUTHORIZED: 342 alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED); 343 if (newResource != null) { 344 thePreResourceShowDetails.setResource(i, newResource); 345 } 346 continue; 347 case REJECT: 348 alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT); 349 if (nextOutcome.getOperationOutcome() != null) { 350 IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome(); 351 thePreResourceShowDetails.setResource(i, newOperationOutcome); 352 alreadySeenResources.put(newOperationOutcome, ConsentOperationStatusEnum.PROCEED); 353 } else { 354 resource = null; 355 thePreResourceShowDetails.setResource(i, null); 356 } 357 continue; 358 } 359 } 360 } 361 } 362 363 @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE) 364 public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails) { 365 if (theResponseDetails.getResponseResource() == null) { 366 return; 367 } 368 if (isRequestAuthorized(theRequestDetails)) { 369 return; 370 } 371 if (isAllowListedRequest(theRequestDetails)) { 372 return; 373 } 374 if (isSkipServiceForRequest(theRequestDetails)) { 375 return; 376 } 377 if (myConsentService.isEmpty()) { 378 return; 379 } 380 381 // Take care of outer resource first 382 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 383 getAlreadySeenResourcesMap(theRequestDetails); 384 if (alreadySeenResources.containsKey(theResponseDetails.getResponseResource())) { 385 // we've already seen this resource before 386 ConsentOperationStatusEnum decisionOnResource = 387 alreadySeenResources.get(theResponseDetails.getResponseResource()); 388 389 if (ConsentOperationStatusEnum.AUTHORIZED.equals(decisionOnResource) 390 || ConsentOperationStatusEnum.REJECT.equals(decisionOnResource)) { 391 // the consent service decision on the resource was AUTHORIZED or REJECT. 392 // In both cases, we can immediately return without checking children 393 return; 394 } 395 } else { 396 // we haven't seen this resource before 397 // mark it as seen now, set the initial consent decision value to PROCEED by default, 398 // we will update if it changes another value below 399 alreadySeenResources.put(theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.PROCEED); 400 401 for (IConsentService next : myConsentService) { 402 final ConsentOutcome outcome = next.willSeeResource( 403 theRequestDetails, theResponseDetails.getResponseResource(), myContextConsentServices); 404 if (outcome.getResource() != null) { 405 theResponseDetails.setResponseResource(outcome.getResource()); 406 } 407 408 // Clear the total 409 if (theResponseDetails.getResponseResource() instanceof IBaseBundle) { 410 BundleUtil.setTotal( 411 theRequestDetails.getFhirContext(), 412 (IBaseBundle) theResponseDetails.getResponseResource(), 413 null); 414 } 415 416 switch (outcome.getStatus()) { 417 case REJECT: 418 alreadySeenResources.put( 419 theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.REJECT); 420 if (outcome.getOperationOutcome() != null) { 421 theResponseDetails.setResponseResource(outcome.getOperationOutcome()); 422 } else { 423 theResponseDetails.setResponseResource(null); 424 theResponseDetails.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT); 425 } 426 // Return immediately 427 return; 428 case AUTHORIZED: 429 alreadySeenResources.put( 430 theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.AUTHORIZED); 431 // Don't check children, so return immediately 432 return; 433 case PROCEED: 434 // Check children, so proceed 435 break; 436 } 437 } 438 } 439 440 // See child resources 441 IBaseResource outerResource = theResponseDetails.getResponseResource(); 442 FhirContext ctx = theRequestDetails.getServer().getFhirContext(); 443 IModelVisitor2 visitor = new IModelVisitor2() { 444 @Override 445 public boolean acceptElement( 446 IBase theElement, 447 List<IBase> theContainingElementPath, 448 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 449 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 450 451 // Clear the total 452 if (theElement instanceof IBaseBundle) { 453 BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theElement, null); 454 } 455 456 if (theElement == outerResource) { 457 return true; 458 } 459 if (theElement instanceof IBaseResource) { 460 IBaseResource resource = (IBaseResource) theElement; 461 if (alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) { 462 return true; 463 } 464 465 boolean shouldCheckChildren = true; 466 for (IConsentService next : myConsentService) { 467 ConsentOutcome childOutcome = 468 next.willSeeResource(theRequestDetails, resource, myContextConsentServices); 469 470 IBaseResource replacementResource = null; 471 boolean shouldReplaceResource = false; 472 473 switch (childOutcome.getStatus()) { 474 case REJECT: 475 replacementResource = childOutcome.getOperationOutcome(); 476 shouldReplaceResource = true; 477 alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT); 478 break; 479 case PROCEED: 480 replacementResource = childOutcome.getResource(); 481 shouldReplaceResource = replacementResource != null; 482 break; 483 case AUTHORIZED: 484 replacementResource = childOutcome.getResource(); 485 shouldReplaceResource = replacementResource != null; 486 shouldCheckChildren = false; 487 alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED); 488 break; 489 } 490 491 if (shouldReplaceResource) { 492 IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2); 493 BaseRuntimeChildDefinition containerChildElement = 494 theChildDefinitionPath.get(theChildDefinitionPath.size() - 1); 495 containerChildElement.getMutator().setValue(container, replacementResource); 496 resource = replacementResource; 497 } 498 } 499 500 return shouldCheckChildren; 501 } 502 503 return true; 504 } 505 506 @Override 507 public boolean acceptUndeclaredExtension( 508 IBaseExtension<?, ?> theNextExt, 509 List<IBase> theContainingElementPath, 510 List<BaseRuntimeChildDefinition> theChildDefinitionPath, 511 List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 512 return true; 513 } 514 }; 515 ctx.newTerser().visit(outerResource, visitor); 516 } 517 518 @Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION) 519 public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) { 520 theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE); 521 for (IConsentService next : myConsentService) { 522 next.completeOperationFailure(theRequest, theException, myContextConsentServices); 523 } 524 } 525 526 @Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY) 527 public void requestSucceeded(RequestDetails theRequest) { 528 if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) { 529 return; 530 } 531 for (IConsentService next : myConsentService) { 532 next.completeOperationSuccess(theRequest, myContextConsentServices); 533 } 534 } 535 536 protected RequestDetails getRequestDetailsForCurrentExportOperation( 537 BulkExportJobParameters theParameters, IBaseResource theBaseResource) { 538 // bulk exports are system operations 539 SystemRequestDetails details = new SystemRequestDetails(); 540 return details; 541 } 542 543 @Hook(value = Pointcut.STORAGE_BULK_EXPORT_RESOURCE_INCLUSION) 544 public boolean shouldBulkExportIncludeResource(BulkExportJobParameters theParameters, IBaseResource theResource) { 545 RequestDetails requestDetails = getRequestDetailsForCurrentExportOperation(theParameters, theResource); 546 547 for (IConsentService next : myConsentService) { 548 ConsentOutcome nextOutcome = next.willSeeResource(requestDetails, theResource, myContextConsentServices); 549 550 ConsentOperationStatusEnum status = nextOutcome.getStatus(); 551 switch (status) { 552 case AUTHORIZED: 553 case PROCEED: 554 // go to the next 555 break; 556 case REJECT: 557 // if any consent service rejects, 558 // reject the resource 559 return false; 560 } 561 } 562 563 // default is to include the resource 564 return true; 565 } 566 567 private boolean isRequestAuthorized(RequestDetails theRequestDetails) { 568 boolean retVal = false; 569 if (theRequestDetails != null) { 570 Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey); 571 retVal = Boolean.TRUE.equals(authorizedObj); 572 } 573 return retVal; 574 } 575 576 private boolean isSkipServiceForRequest(RequestDetails theRequestDetails) { 577 return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails); 578 } 579 580 private boolean isAllowListedRequest(RequestDetails theRequestDetails) { 581 return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails); 582 } 583 584 private boolean isMetaOperation(RequestDetails theRequestDetails) { 585 return theRequestDetails != null && OPERATION_META.equals(theRequestDetails.getOperation()); 586 } 587 588 private boolean isMetadataPath(RequestDetails theRequestDetails) { 589 return theRequestDetails != null && URL_TOKEN_METADATA.equals(theRequestDetails.getRequestPath()); 590 } 591 592 private void validateParameter(Map<String, String[]> theParameterMap) { 593 if (theParameterMap != null) { 594 if (theParameterMap.containsKey(Constants.PARAM_SEARCH_TOTAL_MODE) 595 && Arrays.stream(theParameterMap.get("_total")).anyMatch("accurate"::equals)) { 596 throw new InvalidRequestException(Msg.code(2037) + Constants.PARAM_SEARCH_TOTAL_MODE 597 + "=accurate is not permitted on this server"); 598 } 599 if (theParameterMap.containsKey(Constants.PARAM_SUMMARY) 600 && Arrays.stream(theParameterMap.get("_summary")).anyMatch("count"::equals)) { 601 throw new InvalidRequestException( 602 Msg.code(2038) + Constants.PARAM_SUMMARY + "=count is not permitted on this server"); 603 } 604 } 605 } 606 607 /** 608 * The map returned by this method keeps track of the resources already processed by ConsentInterceptor in the 609 * context of a request. 610 * If the map contains a particular resource, it means that the resource has already been processed and the value 611 * is the status returned by consent services for that resource. 612 * @param theRequestDetails 613 * @return 614 */ 615 @SuppressWarnings("unchecked") 616 private IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> getAlreadySeenResourcesMap( 617 RequestDetails theRequestDetails) { 618 IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources = 619 (IdentityHashMap<IBaseResource, ConsentOperationStatusEnum>) 620 theRequestDetails.getUserData().get(myRequestSeenResourcesKey); 621 if (alreadySeenResources == null) { 622 alreadySeenResources = new IdentityHashMap<>(); 623 theRequestDetails.getUserData().put(myRequestSeenResourcesKey, alreadySeenResources); 624 } 625 return alreadySeenResources; 626 } 627 628 private static ForbiddenOperationException toForbiddenOperationException(ConsentOutcome theOutcome) { 629 IBaseOperationOutcome operationOutcome = null; 630 if (theOutcome.getOperationOutcome() != null) { 631 operationOutcome = theOutcome.getOperationOutcome(); 632 } 633 return new ForbiddenOperationException("Rejected by consent service", operationOutcome); 634 } 635}