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