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        private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
084        private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class);
085        private static final Set<BundleTypeEnum> STANDALONE_BUNDLE_RESOURCE_TYPES =
086                        Set.of(BundleTypeEnum.DOCUMENT, BundleTypeEnum.MESSAGE);
087
088        private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
089        private final String myRequestSeenResourcesKey =
090                        AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
091        private final String myRequestRuleListKey =
092                        AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_RULELIST";
093        private PolicyEnum myDefaultPolicy = PolicyEnum.DENY;
094        private Set<AuthorizationFlagsEnum> myFlags = Collections.emptySet();
095        private IValidationSupport myValidationSupport;
096
097        private IAuthorizationSearchParamMatcher myAuthorizationSearchParamMatcher;
098        private Logger myTroubleshootingLog;
099
100        /**
101         * Constructor
102         */
103        public AuthorizationInterceptor() {
104                super();
105                setTroubleshootingLog(ourLog);
106        }
107
108        /**
109         * Constructor
110         *
111         * @param theDefaultPolicy The default policy if no rules apply (must not be null)
112         */
113        public AuthorizationInterceptor(PolicyEnum theDefaultPolicy) {
114                this();
115                setDefaultPolicy(theDefaultPolicy);
116        }
117
118        @Nonnull
119        @Override
120        public Logger getTroubleshootingLog() {
121                return myTroubleshootingLog;
122        }
123
124        public void setTroubleshootingLog(@Nonnull Logger theTroubleshootingLog) {
125                Validate.notNull(theTroubleshootingLog, "theTroubleshootingLog must not be null");
126                myTroubleshootingLog = theTroubleshootingLog;
127        }
128
129        private void applyRulesAndFailIfDeny(
130                        RestOperationTypeEnum theOperation,
131                        RequestDetails theRequestDetails,
132                        IBaseResource theInputResource,
133                        IIdType theInputResourceId,
134                        IBaseResource theOutputResource,
135                        Pointcut thePointcut) {
136                Verdict decision = applyRulesAndReturnDecision(
137                                theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, thePointcut);
138
139                if (decision.getDecision() == PolicyEnum.ALLOW) {
140                        return;
141                }
142
143                handleDeny(theRequestDetails, decision);
144        }
145
146        @Override
147        public Verdict applyRulesAndReturnDecision(
148                        RestOperationTypeEnum theOperation,
149                        RequestDetails theRequestDetails,
150                        IBaseResource theInputResource,
151                        IIdType theInputResourceId,
152                        IBaseResource theOutputResource,
153                        Pointcut thePointcut) {
154                @SuppressWarnings("unchecked")
155                List<IAuthRule> rules =
156                                (List<IAuthRule>) theRequestDetails.getUserData().get(myRequestRuleListKey);
157                if (rules == null) {
158                        rules = buildRuleList(theRequestDetails);
159                        theRequestDetails.getUserData().put(myRequestRuleListKey, rules);
160                }
161                Set<AuthorizationFlagsEnum> flags = getFlags();
162
163                ourLog.trace(
164                                "Applying {} rules to render an auth decision for operation {}, theInputResource type={}, theOutputResource type={}, thePointcut={} ",
165                                rules.size(),
166                                getPointcutNameOrEmpty(thePointcut),
167                                getResourceTypeOrEmpty(theInputResource),
168                                getResourceTypeOrEmpty(theOutputResource));
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 =
512                                ConsentInterceptor.getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
513                if (alreadySeenMap.putIfAbsent(theResponseObject, Boolean.TRUE) != null) {
514                        return;
515                }
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 = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext);
534                                }
535                                break;
536                        }
537                        default: {
538                                if (theResponseObject != null) {
539                                        resources = Collections.singletonList(theResponseObject);
540                                }
541                                break;
542                        }
543                }
544
545                for (IBaseResource nextResponse : resources) {
546                        applyRulesAndFailIfDeny(
547                                        theRequestDetails.getRestOperationType(), theRequestDetails, null, null, nextResponse, thePointcut);
548                }
549        }
550
551        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
552        public void hookResourcePreCreate(RequestDetails theRequest, IBaseResource theResource, Pointcut thePointcut) {
553                handleUserOperation(theRequest, theResource, RestOperationTypeEnum.CREATE, thePointcut);
554        }
555
556        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED)
557        public void hookResourcePreDelete(RequestDetails theRequest, IBaseResource theResource, Pointcut thePointcut) {
558                handleUserOperation(theRequest, theResource, RestOperationTypeEnum.DELETE, thePointcut);
559        }
560
561        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
562        public void hookResourcePreUpdate(
563                        RequestDetails theRequest,
564                        IBaseResource theOldResource,
565                        IBaseResource theNewResource,
566                        Pointcut thePointcut) {
567                if (theOldResource != null) {
568                        handleUserOperation(theRequest, theOldResource, RestOperationTypeEnum.UPDATE, thePointcut);
569                }
570                handleUserOperation(theRequest, theNewResource, RestOperationTypeEnum.UPDATE, thePointcut);
571        }
572
573        private enum OperationExamineDirection {
574                BOTH,
575                IN,
576                NONE,
577                OUT,
578        }
579
580        protected static List<IBaseResource> toListOfResourcesAndExcludeContainer(
581                        IBaseResource theResponseObject, FhirContext fhirContext) {
582                if (theResponseObject == null) {
583                        return Collections.emptyList();
584                }
585
586                List<IBaseResource> retVal;
587
588                boolean shouldExamineChildResources = shouldExamineChildResources(theResponseObject, fhirContext);
589                if (!shouldExamineChildResources) {
590                        return Collections.singletonList(theResponseObject);
591                }
592
593                retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class);
594
595                // Exclude the container
596                if (!retVal.isEmpty() && retVal.get(0) == theResponseObject) {
597                        retVal = retVal.subList(1, retVal.size());
598                }
599
600                // Don't apply security to OperationOutcome
601                retVal.removeIf(t -> t instanceof IBaseOperationOutcome);
602
603                return retVal;
604        }
605
606        /**
607         * This method determines if the given Resource should have permissions applied to the resources inside or
608         * to the Resource itself.
609         * For Parameters resources, we include child resources when checking the permissions.
610         * For Bundle resources, we only look at resources inside if the Bundle is of type document, collection, or message.
611         */
612        protected static boolean shouldExamineChildResources(IBaseResource theResource, FhirContext theFhirContext) {
613                if (theResource instanceof IBaseParameters) {
614                        return true;
615                }
616                if (theResource instanceof IBaseBundle) {
617                        BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, ((IBaseBundle) theResource));
618                        boolean isStandaloneBundleResource =
619                                        bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType);
620                        return !isStandaloneBundleResource;
621                }
622                return false;
623        }
624
625        public static class Verdict {
626
627                private final IAuthRule myDecidingRule;
628                private final PolicyEnum myDecision;
629
630                public Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) {
631                        Validate.notNull(theDecision);
632
633                        myDecision = theDecision;
634                        myDecidingRule = theDecidingRule;
635                }
636
637                IAuthRule getDecidingRule() {
638                        return myDecidingRule;
639                }
640
641                public PolicyEnum getDecision() {
642                        return myDecision;
643                }
644
645                @Override
646                public String toString() {
647                        ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
648                        String ruleName;
649                        if (myDecidingRule != null) {
650                                ruleName = myDecidingRule.getName();
651                        } else {
652                                ruleName = "(none)";
653                        }
654                        b.append("rule", ruleName);
655                        b.append("decision", myDecision.name());
656                        return b.build();
657                }
658        }
659
660        private Object getPointcutNameOrEmpty(Pointcut thePointcut) {
661                return nonNull(thePointcut) ? thePointcut.name() : EMPTY;
662        }
663
664        private String getResourceTypeOrEmpty(IBaseResource theResource) {
665                String retVal = StringUtils.EMPTY;
666
667                if (isNull(theResource)) {
668                        return retVal;
669                }
670
671                if (isNull(theResource.getIdElement())) {
672                        return retVal;
673                }
674
675                if (isNull(theResource.getIdElement().getResourceType())) {
676                        return retVal;
677                }
678
679                return theResource.getIdElement().getResourceType();
680        }
681}