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