
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2026 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.auth; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.support.IValidationSupport; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.api.Hook; 026import ca.uhn.fhir.interceptor.api.Interceptor; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.model.valueset.BundleTypeEnum; 029import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 030import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 031import ca.uhn.fhir.rest.api.server.RequestDetails; 032import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters; 033import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; 034import ca.uhn.fhir.util.BundleUtil; 035import com.google.common.collect.Lists; 036import jakarta.annotation.Nonnull; 037import jakarta.annotation.Nullable; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.commons.lang3.Validate; 040import org.apache.commons.lang3.builder.ToStringBuilder; 041import org.apache.commons.lang3.builder.ToStringStyle; 042import org.hl7.fhir.instance.model.api.IBaseBundle; 043import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 044import org.hl7.fhir.instance.model.api.IBaseParameters; 045import org.hl7.fhir.instance.model.api.IBaseResource; 046import org.hl7.fhir.instance.model.api.IIdType; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049 050import java.util.ArrayList; 051import java.util.Collection; 052import java.util.Collections; 053import java.util.HashSet; 054import java.util.IdentityHashMap; 055import java.util.List; 056import java.util.Objects; 057import java.util.Set; 058import java.util.concurrent.atomic.AtomicInteger; 059 060import static java.util.Objects.isNull; 061import static java.util.Objects.nonNull; 062import static org.apache.commons.lang3.StringUtils.EMPTY; 063import static org.apache.commons.lang3.StringUtils.isNotBlank; 064 065/** 066 * This class is a base class for interceptors which can be used to 067 * inspect requests and responses to determine whether the calling user 068 * has permission to perform the given action. 069 * <p> 070 * See the HAPI FHIR 071 * <a href="https://hapifhir.io/hapi-fhir/docs/security/introduction.html">Documentation on Server Security</a> 072 * for information on how to use this interceptor. 073 * </p> 074 * 075 * @see SearchNarrowingInterceptor 076 */ 077@SuppressWarnings("unused") 078@Interceptor(order = AuthorizationConstants.ORDER_AUTH_INTERCEPTOR) 079public class AuthorizationInterceptor implements IRuleApplier { 080 081 public static final String REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS = 082 AuthorizationInterceptor.class.getName() + "_BulkDataExportOptions"; 083 private static final AtomicInteger ourInstanceCount = new AtomicInteger(0); 084 private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class); 085 private static final Set<BundleTypeEnum> STANDALONE_BUNDLE_RESOURCE_TYPES = 086 Set.of(BundleTypeEnum.DOCUMENT, BundleTypeEnum.MESSAGE, BundleTypeEnum.COLLECTION); 087 088 private final int myInstanceIndex = ourInstanceCount.incrementAndGet(); 089 private final String myRequestSeenResourcesKey = 090 AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES"; 091 private final String myRequestRuleListKey = 092 AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_RULELIST"; 093 private PolicyEnum myDefaultPolicy = PolicyEnum.DENY; 094 private Set<AuthorizationFlagsEnum> myFlags = Collections.emptySet(); 095 public static final List<RestOperationTypeEnum> REST_OPERATIONS_TO_EXCLUDE_SECURITY_FOR_OPERATION_OUTCOME = List.of( 096 RestOperationTypeEnum.SEARCH_TYPE, RestOperationTypeEnum.SEARCH_SYSTEM, RestOperationTypeEnum.GET_PAGE); 097 private IValidationSupport myValidationSupport; 098 099 private IAuthorizationSearchParamMatcher myAuthorizationSearchParamMatcher; 100 private Logger myTroubleshootingLog; 101 102 /** 103 * Constructor 104 */ 105 public AuthorizationInterceptor() { 106 super(); 107 setTroubleshootingLog(ourLog); 108 } 109 110 /** 111 * Constructor 112 * 113 * @param theDefaultPolicy The default policy if no rules apply (must not be null) 114 */ 115 public AuthorizationInterceptor(PolicyEnum theDefaultPolicy) { 116 this(); 117 setDefaultPolicy(theDefaultPolicy); 118 } 119 120 @Nonnull 121 @Override 122 public Logger getTroubleshootingLog() { 123 return myTroubleshootingLog; 124 } 125 126 public void setTroubleshootingLog(@Nonnull Logger theTroubleshootingLog) { 127 Validate.notNull(theTroubleshootingLog, "theTroubleshootingLog must not be null"); 128 myTroubleshootingLog = theTroubleshootingLog; 129 } 130 131 private void applyRulesAndFailIfDeny( 132 RestOperationTypeEnum theOperation, 133 RequestDetails theRequestDetails, 134 IBaseResource theInputResource, 135 IIdType theInputResourceId, 136 IBaseResource theOutputResource, 137 Pointcut thePointcut) { 138 Verdict decision = applyRulesAndReturnDecision( 139 theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, thePointcut); 140 141 if (decision.getDecision() == PolicyEnum.ALLOW) { 142 return; 143 } 144 145 handleDeny(theRequestDetails, decision); 146 } 147 148 @Override 149 public Verdict applyRulesAndReturnDecision( 150 RestOperationTypeEnum theOperation, 151 RequestDetails theRequestDetails, 152 IBaseResource theInputResource, 153 IIdType theInputResourceId, 154 IBaseResource theOutputResource, 155 Pointcut thePointcut) { 156 @SuppressWarnings("unchecked") 157 List<IAuthRule> rules = 158 (List<IAuthRule>) theRequestDetails.getUserData().get(myRequestRuleListKey); 159 if (rules == null) { 160 rules = buildRuleList(theRequestDetails); 161 theRequestDetails.getUserData().put(myRequestRuleListKey, rules); 162 } 163 Set<AuthorizationFlagsEnum> flags = getFlags(); 164 165 ourLog.trace( 166 "Applying {} rules to render an auth decision for operation {}, theInputResource type={}, theOutputResource type={}, thePointcut={} ", 167 rules.size(), 168 getPointcutNameOrEmpty(thePointcut), 169 getResourceTypeOrEmpty(theInputResource), 170 getResourceTypeOrEmpty(theOutputResource), 171 thePointcut); 172 173 Verdict verdict = null; 174 for (IAuthRule nextRule : rules) { 175 ourLog.trace("Rule being applied - {}", nextRule); 176 verdict = nextRule.applyRule( 177 theOperation, 178 theRequestDetails, 179 theInputResource, 180 theInputResourceId, 181 theOutputResource, 182 this, 183 flags, 184 thePointcut); 185 if (verdict != null) { 186 ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision()); 187 break; 188 } 189 } 190 191 if (verdict == null) { 192 ourLog.trace("No rules returned a decision, applying default {}", myDefaultPolicy); 193 return new Verdict(getDefaultPolicy(), null); 194 } 195 196 return verdict; 197 } 198 199 /** 200 * @since 6.0.0 201 */ 202 @Nullable 203 @Override 204 public IValidationSupport getValidationSupport() { 205 return myValidationSupport; 206 } 207 208 /** 209 * Sets a validation support module that will be used for terminology-based rules 210 * 211 * @param theValidationSupport The validation support. Null is also acceptable (this is the default), 212 * in which case the validation support module associated with the {@link FhirContext} 213 * will be used. 214 * @since 6.0.0 215 */ 216 public AuthorizationInterceptor setValidationSupport(IValidationSupport theValidationSupport) { 217 myValidationSupport = theValidationSupport; 218 return this; 219 } 220 221 /** 222 * Sets a search parameter matcher for use in handling SMART v2 filter scopes 223 * 224 * @param theAuthorizationSearchParamMatcher The search parameter matcher. Defaults to null. 225 */ 226 public void setAuthorizationSearchParamMatcher( 227 @Nullable IAuthorizationSearchParamMatcher theAuthorizationSearchParamMatcher) { 228 this.myAuthorizationSearchParamMatcher = theAuthorizationSearchParamMatcher; 229 } 230 231 @Override 232 @Nullable 233 public IAuthorizationSearchParamMatcher getSearchParamMatcher() { 234 return myAuthorizationSearchParamMatcher; 235 } 236 237 /** 238 * Subclasses should override this method to supply the set of rules to be applied to 239 * this individual request. 240 * <p> 241 * Typically this is done by examining <code>theRequestDetails</code> to find 242 * out who the current user is and then using a {@link RuleBuilder} to create 243 * an appropriate rule chain. 244 * </p> 245 * 246 * @param theRequestDetails The individual request currently being applied 247 */ 248 public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { 249 return new ArrayList<>(); 250 } 251 252 private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation) { 253 254 switch (theOperation) { 255 case ADD_TAGS: 256 case DELETE_TAGS: 257 case GET_TAGS: 258 // These are DSTU1 operations and not relevant 259 return OperationExamineDirection.NONE; 260 261 case EXTENDED_OPERATION_INSTANCE: 262 case EXTENDED_OPERATION_SERVER: 263 case EXTENDED_OPERATION_TYPE: 264 return OperationExamineDirection.BOTH; 265 266 case METADATA: 267 // Security does not apply to these operations 268 return OperationExamineDirection.IN; 269 270 case DELETE: 271 // Delete is a special case 272 return OperationExamineDirection.IN; 273 274 case CREATE: 275 case UPDATE: 276 case PATCH: 277 return OperationExamineDirection.IN; 278 279 case META: 280 case META_ADD: 281 case META_DELETE: 282 // meta operations do not apply yet 283 return OperationExamineDirection.NONE; 284 285 case GET_PAGE: 286 case HISTORY_INSTANCE: 287 case HISTORY_SYSTEM: 288 case HISTORY_TYPE: 289 case READ: 290 case SEARCH_SYSTEM: 291 case SEARCH_TYPE: 292 case VREAD: 293 return OperationExamineDirection.OUT; 294 295 case TRANSACTION: 296 return OperationExamineDirection.BOTH; 297 298 case VALIDATE: 299 // Nothing yet 300 return OperationExamineDirection.NONE; 301 302 case GRAPHQL_REQUEST: 303 return OperationExamineDirection.BOTH; 304 305 default: 306 // Should not happen 307 throw new IllegalStateException( 308 Msg.code(332) + "Unable to apply security to event of type " + theOperation); 309 } 310 } 311 312 /** 313 * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY} 314 */ 315 public PolicyEnum getDefaultPolicy() { 316 return myDefaultPolicy; 317 } 318 319 /** 320 * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY} 321 * 322 * @param theDefaultPolicy The policy (must not be <code>null</code>) 323 */ 324 public AuthorizationInterceptor setDefaultPolicy(PolicyEnum theDefaultPolicy) { 325 Validate.notNull(theDefaultPolicy, "theDefaultPolicy must not be null"); 326 myDefaultPolicy = theDefaultPolicy; 327 return this; 328 } 329 330 /** 331 * This property configures any flags affecting how authorization is 332 * applied. By default no flags are applied. 333 * 334 * @see #setFlags(Collection) 335 */ 336 public Set<AuthorizationFlagsEnum> getFlags() { 337 return Collections.unmodifiableSet(myFlags); 338 } 339 340 /** 341 * This property configures any flags affecting how authorization is 342 * applied. By default no flags are applied. 343 * 344 * @param theFlags The flags (must not be null) 345 * @see #setFlags(AuthorizationFlagsEnum...) 346 */ 347 public AuthorizationInterceptor setFlags(Collection<AuthorizationFlagsEnum> theFlags) { 348 Validate.notNull(theFlags, "theFlags must not be null"); 349 myFlags = new HashSet<>(theFlags); 350 return this; 351 } 352 353 /** 354 * This property configures any flags affecting how authorization is 355 * applied. By default no flags are applied. 356 * 357 * @param theFlags The flags (must not be null) 358 * @see #setFlags(Collection) 359 */ 360 public AuthorizationInterceptor setFlags(AuthorizationFlagsEnum... theFlags) { 361 Validate.notNull(theFlags, "theFlags must not be null"); 362 return setFlags(Lists.newArrayList(theFlags)); 363 } 364 365 /** 366 * Handle an access control verdict of {@link PolicyEnum#DENY}. 367 * <p> 368 * Subclasses may override to implement specific behaviour, but default is to 369 * throw {@link ForbiddenOperationException} (HTTP 403) with error message citing the 370 * rule name which trigered failure 371 * </p> 372 * 373 * @since HAPI FHIR 3.6.0 374 */ 375 protected void handleDeny(RequestDetails theRequestDetails, Verdict decision) { 376 handleDeny(decision); 377 } 378 379 /** 380 * This method should not be overridden. As of HAPI FHIR 3.6.0, you 381 * should override {@link #handleDeny(RequestDetails, Verdict)} instead. This 382 * method will be removed in the future. 383 */ 384 protected void handleDeny(Verdict decision) { 385 if (decision.getDecidingRule() != null) { 386 String ruleName = Objects.toString(decision.getDecidingRule().getName(), "(unnamed rule)"); 387 throw new ForbiddenOperationException(Msg.code(333) + "Access denied by rule: " + ruleName); 388 } 389 throw new ForbiddenOperationException(Msg.code(334) + "Access denied by default policy (no applicable rules)"); 390 } 391 392 private void handleUserOperation( 393 RequestDetails theRequest, 394 IBaseResource theResource, 395 RestOperationTypeEnum theOperation, 396 Pointcut thePointcut) { 397 applyRulesAndFailIfDeny(theOperation, theRequest, theResource, theResource.getIdElement(), null, thePointcut); 398 } 399 400 @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED) 401 public void incomingRequestPreHandled(RequestDetails theRequest, Pointcut thePointcut) { 402 IBaseResource inputResource = null; 403 IIdType inputResourceId = null; 404 405 switch (determineOperationDirection(theRequest.getRestOperationType())) { 406 case IN: 407 case BOTH: 408 inputResource = theRequest.getResource(); 409 inputResourceId = theRequest.getId(); 410 if (inputResourceId == null && isNotBlank(theRequest.getResourceName())) { 411 inputResourceId = theRequest.getFhirContext().getVersion().newIdType(); 412 inputResourceId.setParts(null, theRequest.getResourceName(), null, null); 413 } 414 break; 415 case OUT: 416 // inputResource = null; 417 inputResourceId = theRequest.getId(); 418 break; 419 case NONE: 420 return; 421 } 422 423 applyRulesAndFailIfDeny( 424 theRequest.getRestOperationType(), theRequest, inputResource, inputResourceId, null, thePointcut); 425 } 426 427 @Hook(Pointcut.STORAGE_PRESHOW_RESOURCES) 428 public void hookPreShow( 429 RequestDetails theRequestDetails, IPreResourceShowDetails theDetails, Pointcut thePointcut) { 430 for (int i = 0; i < theDetails.size(); i++) { 431 IBaseResource next = theDetails.getResource(i); 432 checkOutgoingResourceAndFailIfDeny(theRequestDetails, next, thePointcut); 433 } 434 } 435 436 @Hook(Pointcut.SERVER_OUTGOING_RESPONSE) 437 public void hookOutgoingResponse( 438 RequestDetails theRequestDetails, IBaseResource theResponseObject, Pointcut thePointcut) { 439 checkOutgoingResourceAndFailIfDeny(theRequestDetails, theResponseObject, thePointcut); 440 } 441 442 @Hook(Pointcut.STORAGE_CASCADE_DELETE) 443 public void hookCascadeDeleteForConflict( 444 RequestDetails theRequestDetails, Pointcut thePointcut, IBaseResource theResourceToDelete) { 445 Objects.requireNonNull(theResourceToDelete); // just in case 446 checkPointcutAndFailIfDeny(theRequestDetails, thePointcut, theResourceToDelete); 447 } 448 449 @Hook(Pointcut.STORAGE_PRE_DELETE_EXPUNGE) 450 public void hookDeleteExpunge(RequestDetails theRequestDetails, Pointcut thePointcut) { 451 applyRulesAndFailIfDeny( 452 theRequestDetails.getRestOperationType(), theRequestDetails, null, null, null, thePointcut); 453 } 454 455 @Hook(Pointcut.STORAGE_INITIATE_BULK_EXPORT) 456 public void initiateBulkExport( 457 RequestDetails theRequestDetails, BulkExportJobParameters theBulkExportOptions, Pointcut thePointcut) { 458 // RestOperationTypeEnum restOperationType = 459 // determineRestOperationTypeFromBulkExportOptions(theBulkExportOptions); 460 RestOperationTypeEnum restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 461 462 if (theRequestDetails != null) { 463 theRequestDetails.getUserData().put(REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS, theBulkExportOptions); 464 } 465 applyRulesAndFailIfDeny(restOperationType, theRequestDetails, null, null, null, thePointcut); 466 } 467 468 /** 469 * TODO GGG This method should eventually be used when invoking the rules applier.....however we currently rely on the incorrect 470 * behaviour of passing down `EXTENDED_OPERATION_SERVER`. 471 */ 472 private RestOperationTypeEnum determineRestOperationTypeFromBulkExportOptions( 473 BulkExportJobParameters theBulkExportOptions) { 474 RestOperationTypeEnum restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 475 BulkExportJobParameters.ExportStyle exportStyle = theBulkExportOptions.getExportStyle(); 476 if (exportStyle.equals(BulkExportJobParameters.ExportStyle.PATIENT)) { 477 if (theBulkExportOptions.getPatientIds().size() == 1) { 478 restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 479 } else { 480 restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 481 } 482 } else if (exportStyle.equals(BulkExportJobParameters.ExportStyle.GROUP)) { 483 restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 484 } 485 return restOperationType; 486 } 487 488 private void checkPointcutAndFailIfDeny( 489 RequestDetails theRequestDetails, Pointcut thePointcut, @Nonnull IBaseResource theInputResource) { 490 applyRulesAndFailIfDeny( 491 theRequestDetails.getRestOperationType(), 492 theRequestDetails, 493 theInputResource, 494 theInputResource.getIdElement(), 495 null, 496 thePointcut); 497 } 498 499 private void checkOutgoingResourceAndFailIfDeny( 500 RequestDetails theRequestDetails, IBaseResource theResponseObject, Pointcut thePointcut) { 501 502 switch (determineOperationDirection(theRequestDetails.getRestOperationType())) { 503 case IN: 504 case NONE: 505 return; 506 case BOTH: 507 case OUT: 508 break; 509 } 510 511 // Don't check the value twice 512 IdentityHashMap<IBaseResource, Boolean> alreadySeenMap = getAlreadySeenResourcesMap(theRequestDetails); 513 if (alreadySeenMap.putIfAbsent(theResponseObject, Boolean.TRUE) != null) { 514 return; 515 } 516 FhirContext fhirContext = theRequestDetails.getServer().getFhirContext(); 517 List<IBaseResource> resources = Collections.emptyList(); 518 519 //noinspection EnumSwitchStatementWhichMissesCases 520 switch (theRequestDetails.getRestOperationType()) { 521 case SEARCH_SYSTEM: 522 case SEARCH_TYPE: 523 case HISTORY_INSTANCE: 524 case HISTORY_SYSTEM: 525 case HISTORY_TYPE: 526 case TRANSACTION: 527 case GET_PAGE: 528 case EXTENDED_OPERATION_SERVER: 529 case EXTENDED_OPERATION_TYPE: 530 case EXTENDED_OPERATION_INSTANCE: { 531 if (theResponseObject != null) { 532 resources = toListOfResourcesAndExcludeContainerUnlessStandalone( 533 theResponseObject, fhirContext, theRequestDetails); 534 } 535 break; 536 } 537 default: { 538 if (theResponseObject != null) { 539 resources = Collections.singletonList(theResponseObject); 540 } 541 break; 542 } 543 } 544 545 for (IBaseResource nextResponse : resources) { 546 applyRulesAndFailIfDeny( 547 theRequestDetails.getRestOperationType(), theRequestDetails, null, null, nextResponse, thePointcut); 548 } 549 } 550 551 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) 552 public void hookResourcePreCreate(RequestDetails theRequest, IBaseResource theResource, Pointcut thePointcut) { 553 handleUserOperation(theRequest, theResource, RestOperationTypeEnum.CREATE, thePointcut); 554 } 555 556 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED) 557 public void hookResourcePreDelete(RequestDetails theRequest, IBaseResource theResource, Pointcut thePointcut) { 558 handleUserOperation(theRequest, theResource, RestOperationTypeEnum.DELETE, thePointcut); 559 } 560 561 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) 562 public void hookResourcePreUpdate( 563 RequestDetails theRequest, 564 IBaseResource theOldResource, 565 IBaseResource theNewResource, 566 Pointcut thePointcut) { 567 if (theOldResource != null) { 568 handleUserOperation(theRequest, theOldResource, RestOperationTypeEnum.UPDATE, thePointcut); 569 } 570 handleUserOperation(theRequest, theNewResource, RestOperationTypeEnum.UPDATE, thePointcut); 571 } 572 573 private enum OperationExamineDirection { 574 BOTH, 575 IN, 576 NONE, 577 OUT, 578 } 579 580 protected static List<IBaseResource> toListOfResourcesAndExcludeContainerUnlessStandalone( 581 IBaseResource theResponseObject, FhirContext fhirContext, RequestDetails theRequestDetails) { 582 583 if (theResponseObject == null) { 584 return Collections.emptyList(); 585 } 586 587 boolean shouldExamineChildResources = shouldExamineChildResources(theResponseObject, fhirContext); 588 if (!shouldExamineChildResources) { 589 return toListOfResourcesAndExcludeOperationOutcomeBasedOnRestOperationType( 590 theResponseObject, theRequestDetails); 591 } 592 593 return toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext); 594 } 595 596 /** 597 * 598 * @param theResponseObject The resource to convert to a list. 599 * @param theRequestDetails The request details. 600 * @return The response object (a resource) as a list. If the REST operation type in the request details is a 601 * search, and the search is for resources that aren't the OperationOutcome, any OperationOutcome resource is removed from the list. 602 * e.g. A GET [base]/Patient?parameter(s) search may return a bundle containing an OperationOutcome. The OperationOutcome will be removed from the 603 * list to exclude from security. 604 */ 605 private static List<IBaseResource> toListOfResourcesAndExcludeOperationOutcomeBasedOnRestOperationType( 606 IBaseResource theResponseObject, RequestDetails theRequestDetails) { 607 List<IBaseResource> resources = new ArrayList<>(); 608 RestOperationTypeEnum restOperationType = theRequestDetails.getRestOperationType(); 609 String resourceName = theRequestDetails.getResourceName(); 610 resources.add(theResponseObject); 611 612 if (resourceName != null 613 && !resourceName.equals("OperationOutcome") 614 && REST_OPERATIONS_TO_EXCLUDE_SECURITY_FOR_OPERATION_OUTCOME.contains(restOperationType)) { 615 resources.removeIf(t -> t instanceof IBaseOperationOutcome); 616 } 617 618 return resources; 619 } 620 621 @Nonnull 622 public static List<IBaseResource> toListOfResourcesAndExcludeContainer( 623 IBaseResource theResponseObject, FhirContext fhirContext) { 624 List<IBaseResource> retVal; 625 retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); 626 627 // Exclude the container 628 if (!retVal.isEmpty() && retVal.get(0) == theResponseObject) { 629 retVal = retVal.subList(1, retVal.size()); 630 } 631 632 // Don't apply security to OperationOutcome 633 retVal.removeIf(t -> t instanceof IBaseOperationOutcome); 634 635 return retVal; 636 } 637 638 /** 639 * This method determines if the given Resource should have permissions applied 640 * to the resources inside or to the Resource itself. 641 * For Parameters resources, we include child resources when checking the permissions. 642 * For Bundle resources, we look at resources inside if the Bundle type is not in 643 * STANDALONE_BUNDLE_RESOURCE_TYPES set. 644 */ 645 protected static boolean shouldExamineChildResources(IBaseResource theResource, FhirContext theFhirContext) { 646 if (theResource instanceof IBaseParameters) { 647 return true; 648 } 649 650 if (theResource instanceof IBaseBundle baseBundle) { 651 BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, baseBundle); 652 boolean isStandaloneBundleResource = 653 bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType); 654 return !isStandaloneBundleResource; 655 } 656 657 return false; 658 } 659 660 public static class Verdict { 661 662 private final IAuthRule myDecidingRule; 663 private final PolicyEnum myDecision; 664 665 public Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) { 666 Objects.requireNonNull(theDecision); 667 668 myDecision = theDecision; 669 myDecidingRule = theDecidingRule; 670 } 671 672 IAuthRule getDecidingRule() { 673 return myDecidingRule; 674 } 675 676 public PolicyEnum getDecision() { 677 return myDecision; 678 } 679 680 @Override 681 public String toString() { 682 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 683 String ruleName; 684 if (myDecidingRule != null) { 685 ruleName = myDecidingRule.getName(); 686 } else { 687 ruleName = "(none)"; 688 } 689 b.append("rule", ruleName); 690 b.append("decision", myDecision.name()); 691 return b.build(); 692 } 693 } 694 695 private Object getPointcutNameOrEmpty(Pointcut thePointcut) { 696 return nonNull(thePointcut) ? thePointcut.name() : EMPTY; 697 } 698 699 private String getResourceTypeOrEmpty(IBaseResource theResource) { 700 String retVal = StringUtils.EMPTY; 701 702 if (isNull(theResource)) { 703 return retVal; 704 } 705 706 if (isNull(theResource.getIdElement())) { 707 return retVal; 708 } 709 710 if (isNull(theResource.getIdElement().getResourceType())) { 711 return retVal; 712 } 713 714 return theResource.getIdElement().getResourceType(); 715 } 716 717 @SuppressWarnings("unchecked") 718 private IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails) { 719 IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>) 720 theRequestDetails.getUserData().get(myRequestSeenResourcesKey); 721 if (alreadySeenResources == null) { 722 alreadySeenResources = new IdentityHashMap<>(); 723 theRequestDetails.getUserData().put(myRequestSeenResourcesKey, alreadySeenResources); 724 } 725 return alreadySeenResources; 726 } 727}