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