001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2024 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.rest.server.interceptor.consent.ConsentInterceptor; 035import ca.uhn.fhir.util.BundleUtil; 036import com.google.common.collect.Lists; 037import jakarta.annotation.Nonnull; 038import jakarta.annotation.Nullable; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.lang3.Validate; 041import org.apache.commons.lang3.builder.ToStringBuilder; 042import org.apache.commons.lang3.builder.ToStringStyle; 043import org.hl7.fhir.instance.model.api.IBaseBundle; 044import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 045import org.hl7.fhir.instance.model.api.IBaseParameters; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047import org.hl7.fhir.instance.model.api.IIdType; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051import java.util.ArrayList; 052import java.util.Collection; 053import java.util.Collections; 054import java.util.HashSet; 055import java.util.IdentityHashMap; 056import java.util.List; 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.defaultString; 064import static org.apache.commons.lang3.StringUtils.isNotBlank; 065 066/** 067 * This class is a base class for interceptors which can be used to 068 * inspect requests and responses to determine whether the calling user 069 * has permission to perform the given action. 070 * <p> 071 * See the HAPI FHIR 072 * <a href="https://hapifhir.io/hapi-fhir/docs/security/introduction.html">Documentation on Server Security</a> 073 * for information on how to use this interceptor. 074 * </p> 075 * 076 * @see SearchNarrowingInterceptor 077 */ 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 public static final String BUNDLE = "Bundle"; 084 private static final AtomicInteger ourInstanceCount = new AtomicInteger(0); 085 private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class); 086 private static final Set<BundleTypeEnum> STANDALONE_BUNDLE_RESOURCE_TYPES = 087 Set.of(BundleTypeEnum.DOCUMENT, BundleTypeEnum.COLLECTION, BundleTypeEnum.MESSAGE); 088 089 private final int myInstanceIndex = ourInstanceCount.incrementAndGet(); 090 private final String myRequestSeenResourcesKey = 091 AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES"; 092 private final String myRequestRuleListKey = 093 AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_RULELIST"; 094 private PolicyEnum myDefaultPolicy = PolicyEnum.DENY; 095 private Set<AuthorizationFlagsEnum> myFlags = Collections.emptySet(); 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 171 Verdict verdict = null; 172 for (IAuthRule nextRule : rules) { 173 ourLog.trace("Rule being applied - {}", nextRule); 174 verdict = nextRule.applyRule( 175 theOperation, 176 theRequestDetails, 177 theInputResource, 178 theInputResourceId, 179 theOutputResource, 180 this, 181 flags, 182 thePointcut); 183 if (verdict != null) { 184 ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision()); 185 break; 186 } 187 } 188 189 if (verdict == null) { 190 ourLog.trace("No rules returned a decision, applying default {}", myDefaultPolicy); 191 return new Verdict(getDefaultPolicy(), null); 192 } 193 194 return verdict; 195 } 196 197 /** 198 * @since 6.0.0 199 */ 200 @Nullable 201 @Override 202 public IValidationSupport getValidationSupport() { 203 return myValidationSupport; 204 } 205 206 /** 207 * Sets a validation support module that will be used for terminology-based rules 208 * 209 * @param theValidationSupport The validation support. Null is also acceptable (this is the default), 210 * in which case the validation support module associated with the {@link FhirContext} 211 * will be used. 212 * @since 6.0.0 213 */ 214 public AuthorizationInterceptor setValidationSupport(IValidationSupport theValidationSupport) { 215 myValidationSupport = theValidationSupport; 216 return this; 217 } 218 219 /** 220 * Sets a search parameter matcher for use in handling SMART v2 filter scopes 221 * 222 * @param theAuthorizationSearchParamMatcher The search parameter matcher. Defaults to null. 223 */ 224 public void setAuthorizationSearchParamMatcher( 225 @Nullable IAuthorizationSearchParamMatcher theAuthorizationSearchParamMatcher) { 226 this.myAuthorizationSearchParamMatcher = theAuthorizationSearchParamMatcher; 227 } 228 229 @Override 230 @Nullable 231 public IAuthorizationSearchParamMatcher getSearchParamMatcher() { 232 return myAuthorizationSearchParamMatcher; 233 } 234 235 /** 236 * Subclasses should override this method to supply the set of rules to be applied to 237 * this individual request. 238 * <p> 239 * Typically this is done by examining <code>theRequestDetails</code> to find 240 * out who the current user is and then using a {@link RuleBuilder} to create 241 * an appropriate rule chain. 242 * </p> 243 * 244 * @param theRequestDetails The individual request currently being applied 245 */ 246 public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { 247 return new ArrayList<>(); 248 } 249 250 private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation) { 251 252 switch (theOperation) { 253 case ADD_TAGS: 254 case DELETE_TAGS: 255 case GET_TAGS: 256 // These are DSTU1 operations and not relevant 257 return OperationExamineDirection.NONE; 258 259 case EXTENDED_OPERATION_INSTANCE: 260 case EXTENDED_OPERATION_SERVER: 261 case EXTENDED_OPERATION_TYPE: 262 return OperationExamineDirection.BOTH; 263 264 case METADATA: 265 // Security does not apply to these operations 266 return OperationExamineDirection.IN; 267 268 case DELETE: 269 // Delete is a special case 270 return OperationExamineDirection.IN; 271 272 case CREATE: 273 case UPDATE: 274 case PATCH: 275 return OperationExamineDirection.IN; 276 277 case META: 278 case META_ADD: 279 case META_DELETE: 280 // meta operations do not apply yet 281 return OperationExamineDirection.NONE; 282 283 case GET_PAGE: 284 case HISTORY_INSTANCE: 285 case HISTORY_SYSTEM: 286 case HISTORY_TYPE: 287 case READ: 288 case SEARCH_SYSTEM: 289 case SEARCH_TYPE: 290 case VREAD: 291 return OperationExamineDirection.OUT; 292 293 case TRANSACTION: 294 return OperationExamineDirection.BOTH; 295 296 case VALIDATE: 297 // Nothing yet 298 return OperationExamineDirection.NONE; 299 300 case GRAPHQL_REQUEST: 301 return OperationExamineDirection.BOTH; 302 303 default: 304 // Should not happen 305 throw new IllegalStateException( 306 Msg.code(332) + "Unable to apply security to event of type " + theOperation); 307 } 308 } 309 310 /** 311 * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY} 312 */ 313 public PolicyEnum getDefaultPolicy() { 314 return myDefaultPolicy; 315 } 316 317 /** 318 * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY} 319 * 320 * @param theDefaultPolicy The policy (must not be <code>null</code>) 321 */ 322 public AuthorizationInterceptor setDefaultPolicy(PolicyEnum theDefaultPolicy) { 323 Validate.notNull(theDefaultPolicy, "theDefaultPolicy must not be null"); 324 myDefaultPolicy = theDefaultPolicy; 325 return this; 326 } 327 328 /** 329 * This property configures any flags affecting how authorization is 330 * applied. By default no flags are applied. 331 * 332 * @see #setFlags(Collection) 333 */ 334 public Set<AuthorizationFlagsEnum> getFlags() { 335 return Collections.unmodifiableSet(myFlags); 336 } 337 338 /** 339 * This property configures any flags affecting how authorization is 340 * applied. By default no flags are applied. 341 * 342 * @param theFlags The flags (must not be null) 343 * @see #setFlags(AuthorizationFlagsEnum...) 344 */ 345 public AuthorizationInterceptor setFlags(Collection<AuthorizationFlagsEnum> theFlags) { 346 Validate.notNull(theFlags, "theFlags must not be null"); 347 myFlags = new HashSet<>(theFlags); 348 return this; 349 } 350 351 /** 352 * This property configures any flags affecting how authorization is 353 * applied. By default no flags are applied. 354 * 355 * @param theFlags The flags (must not be null) 356 * @see #setFlags(Collection) 357 */ 358 public AuthorizationInterceptor setFlags(AuthorizationFlagsEnum... theFlags) { 359 Validate.notNull(theFlags, "theFlags must not be null"); 360 return setFlags(Lists.newArrayList(theFlags)); 361 } 362 363 /** 364 * Handle an access control verdict of {@link PolicyEnum#DENY}. 365 * <p> 366 * Subclasses may override to implement specific behaviour, but default is to 367 * throw {@link ForbiddenOperationException} (HTTP 403) with error message citing the 368 * rule name which trigered failure 369 * </p> 370 * 371 * @since HAPI FHIR 3.6.0 372 */ 373 protected void handleDeny(RequestDetails theRequestDetails, Verdict decision) { 374 handleDeny(decision); 375 } 376 377 /** 378 * This method should not be overridden. As of HAPI FHIR 3.6.0, you 379 * should override {@link #handleDeny(RequestDetails, Verdict)} instead. This 380 * method will be removed in the future. 381 */ 382 protected void handleDeny(Verdict decision) { 383 if (decision.getDecidingRule() != null) { 384 String ruleName = defaultString(decision.getDecidingRule().getName(), "(unnamed rule)"); 385 throw new ForbiddenOperationException(Msg.code(333) + "Access denied by rule: " + ruleName); 386 } 387 throw new ForbiddenOperationException(Msg.code(334) + "Access denied by default policy (no applicable rules)"); 388 } 389 390 private void handleUserOperation( 391 RequestDetails theRequest, 392 IBaseResource theResource, 393 RestOperationTypeEnum theOperation, 394 Pointcut thePointcut) { 395 applyRulesAndFailIfDeny(theOperation, theRequest, theResource, theResource.getIdElement(), null, thePointcut); 396 } 397 398 @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED) 399 public void incomingRequestPreHandled(RequestDetails theRequest, Pointcut thePointcut) { 400 IBaseResource inputResource = null; 401 IIdType inputResourceId = null; 402 403 switch (determineOperationDirection(theRequest.getRestOperationType())) { 404 case IN: 405 case BOTH: 406 inputResource = theRequest.getResource(); 407 inputResourceId = theRequest.getId(); 408 if (inputResourceId == null && isNotBlank(theRequest.getResourceName())) { 409 inputResourceId = theRequest.getFhirContext().getVersion().newIdType(); 410 inputResourceId.setParts(null, theRequest.getResourceName(), null, null); 411 } 412 break; 413 case OUT: 414 // inputResource = null; 415 inputResourceId = theRequest.getId(); 416 break; 417 case NONE: 418 return; 419 } 420 421 applyRulesAndFailIfDeny( 422 theRequest.getRestOperationType(), theRequest, inputResource, inputResourceId, null, thePointcut); 423 } 424 425 @Hook(Pointcut.STORAGE_PRESHOW_RESOURCES) 426 public void hookPreShow( 427 RequestDetails theRequestDetails, IPreResourceShowDetails theDetails, Pointcut thePointcut) { 428 for (int i = 0; i < theDetails.size(); i++) { 429 IBaseResource next = theDetails.getResource(i); 430 checkOutgoingResourceAndFailIfDeny(theRequestDetails, next, thePointcut); 431 } 432 } 433 434 @Hook(Pointcut.SERVER_OUTGOING_RESPONSE) 435 public void hookOutgoingResponse( 436 RequestDetails theRequestDetails, IBaseResource theResponseObject, Pointcut thePointcut) { 437 checkOutgoingResourceAndFailIfDeny(theRequestDetails, theResponseObject, thePointcut); 438 } 439 440 @Hook(Pointcut.STORAGE_CASCADE_DELETE) 441 public void hookCascadeDeleteForConflict( 442 RequestDetails theRequestDetails, Pointcut thePointcut, IBaseResource theResourceToDelete) { 443 Validate.notNull(theResourceToDelete); // just in case 444 checkPointcutAndFailIfDeny(theRequestDetails, thePointcut, theResourceToDelete); 445 } 446 447 @Hook(Pointcut.STORAGE_PRE_DELETE_EXPUNGE) 448 public void hookDeleteExpunge(RequestDetails theRequestDetails, Pointcut thePointcut) { 449 applyRulesAndFailIfDeny( 450 theRequestDetails.getRestOperationType(), theRequestDetails, null, null, null, thePointcut); 451 } 452 453 @Hook(Pointcut.STORAGE_INITIATE_BULK_EXPORT) 454 public void initiateBulkExport( 455 RequestDetails theRequestDetails, BulkExportJobParameters theBulkExportOptions, Pointcut thePointcut) { 456 // RestOperationTypeEnum restOperationType = 457 // determineRestOperationTypeFromBulkExportOptions(theBulkExportOptions); 458 RestOperationTypeEnum restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 459 460 if (theRequestDetails != null) { 461 theRequestDetails.setAttribute(REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS, theBulkExportOptions); 462 } 463 applyRulesAndFailIfDeny(restOperationType, theRequestDetails, null, null, null, thePointcut); 464 } 465 466 /** 467 * TODO GGG This method should eventually be used when invoking the rules applier.....however we currently rely on the incorrect 468 * behaviour of passing down `EXTENDED_OPERATION_SERVER`. 469 */ 470 private RestOperationTypeEnum determineRestOperationTypeFromBulkExportOptions( 471 BulkExportJobParameters theBulkExportOptions) { 472 RestOperationTypeEnum restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 473 BulkExportJobParameters.ExportStyle exportStyle = theBulkExportOptions.getExportStyle(); 474 if (exportStyle.equals(BulkExportJobParameters.ExportStyle.SYSTEM)) { 475 restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 476 } else 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 = 513 ConsentInterceptor.getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey); 514 if (alreadySeenMap.putIfAbsent(theResponseObject, Boolean.TRUE) != null) { 515 return; 516 } 517 518 FhirContext fhirContext = theRequestDetails.getServer().getFhirContext(); 519 List<IBaseResource> resources = Collections.emptyList(); 520 521 //noinspection EnumSwitchStatementWhichMissesCases 522 switch (theRequestDetails.getRestOperationType()) { 523 case SEARCH_SYSTEM: 524 case SEARCH_TYPE: 525 case HISTORY_INSTANCE: 526 case HISTORY_SYSTEM: 527 case HISTORY_TYPE: 528 case TRANSACTION: 529 case GET_PAGE: 530 case EXTENDED_OPERATION_SERVER: 531 case EXTENDED_OPERATION_TYPE: 532 case EXTENDED_OPERATION_INSTANCE: { 533 if (theResponseObject != null) { 534 resources = toListOfResourcesAndExcludeContainer(theRequestDetails, theResponseObject, fhirContext); 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 public static List<IBaseResource> toListOfResourcesAndExcludeContainer( 582 RequestDetails theRequestDetails, IBaseResource theResponseObject, FhirContext fhirContext) { 583 if (theResponseObject == null) { 584 return Collections.emptyList(); 585 } 586 587 List<IBaseResource> retVal; 588 589 boolean shouldExamineChildResources = false; 590 if (theResponseObject instanceof IBaseBundle) { 591 IBaseBundle bundle = (IBaseBundle) theResponseObject; 592 shouldExamineChildResources = shouldExamineBundleChildResources(theRequestDetails, fhirContext, bundle); 593 } else if (theResponseObject instanceof IBaseParameters) { 594 shouldExamineChildResources = true; 595 } 596 597 if (!shouldExamineChildResources) { 598 return Collections.singletonList(theResponseObject); 599 } 600 601 retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); 602 603 // Exclude the container 604 if (retVal.size() > 0 && retVal.get(0) == theResponseObject) { 605 retVal = retVal.subList(1, retVal.size()); 606 } 607 608 // Don't apply security to OperationOutcome 609 retVal.removeIf(t -> t instanceof IBaseOperationOutcome); 610 611 return retVal; 612 } 613 614 /** 615 * This method determines if the given Bundle should have permissions applied to the resources inside or 616 * to the Bundle itself. 617 * 618 * This distinction is important in Bundle requests where a user has permissions to view all Bundles. In 619 * this scenario we want to apply permissions to the Bundle itself and not the resources inside if 620 * the Bundle is of type document, collection, or message. 621 */ 622 public static boolean shouldExamineBundleChildResources( 623 RequestDetails theRequestDetails, FhirContext theFhirContext, IBaseBundle theBundle) { 624 boolean isBundleRequest = theRequestDetails != null && BUNDLE.equals(theRequestDetails.getResourceName()); 625 if (!isBundleRequest) { 626 return true; 627 } 628 BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, theBundle); 629 boolean isStandaloneBundleResource = 630 bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType); 631 return !isStandaloneBundleResource; 632 } 633 634 public static class Verdict { 635 636 private final IAuthRule myDecidingRule; 637 private final PolicyEnum myDecision; 638 639 public Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) { 640 Validate.notNull(theDecision); 641 642 myDecision = theDecision; 643 myDecidingRule = theDecidingRule; 644 } 645 646 IAuthRule getDecidingRule() { 647 return myDecidingRule; 648 } 649 650 public PolicyEnum getDecision() { 651 return myDecision; 652 } 653 654 @Override 655 public String toString() { 656 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 657 String ruleName; 658 if (myDecidingRule != null) { 659 ruleName = myDecidingRule.getName(); 660 } else { 661 ruleName = "(none)"; 662 } 663 b.append("rule", ruleName); 664 b.append("decision", myDecision.name()); 665 return b.build(); 666 } 667 } 668 669 private Object getPointcutNameOrEmpty(Pointcut thePointcut) { 670 return nonNull(thePointcut) ? thePointcut.name() : EMPTY; 671 } 672 673 private String getResourceTypeOrEmpty(IBaseResource theResource) { 674 String retVal = StringUtils.EMPTY; 675 676 if (isNull(theResource)) { 677 return retVal; 678 } 679 680 if (isNull(theResource.getIdElement())) { 681 return retVal; 682 } 683 684 if (isNull(theResource.getIdElement().getResourceType())) { 685 return retVal; 686 } 687 688 return theResource.getIdElement().getResourceType(); 689 } 690}