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}