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