001/*
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2026 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.Objects;
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.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@SuppressWarnings("unused")
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, BundleTypeEnum.COLLECTION);
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        public static final List<RestOperationTypeEnum> REST_OPERATIONS_TO_EXCLUDE_SECURITY_FOR_OPERATION_OUTCOME = List.of(
096                        RestOperationTypeEnum.SEARCH_TYPE, RestOperationTypeEnum.SEARCH_SYSTEM, RestOperationTypeEnum.GET_PAGE);
097        private IValidationSupport myValidationSupport;
098
099        private IAuthorizationSearchParamMatcher myAuthorizationSearchParamMatcher;
100        private Logger myTroubleshootingLog;
101
102        /**
103         * Constructor
104         */
105        public AuthorizationInterceptor() {
106                super();
107                setTroubleshootingLog(ourLog);
108        }
109
110        /**
111         * Constructor
112         *
113         * @param theDefaultPolicy The default policy if no rules apply (must not be null)
114         */
115        public AuthorizationInterceptor(PolicyEnum theDefaultPolicy) {
116                this();
117                setDefaultPolicy(theDefaultPolicy);
118        }
119
120        @Nonnull
121        @Override
122        public Logger getTroubleshootingLog() {
123                return myTroubleshootingLog;
124        }
125
126        public void setTroubleshootingLog(@Nonnull Logger theTroubleshootingLog) {
127                Validate.notNull(theTroubleshootingLog, "theTroubleshootingLog must not be null");
128                myTroubleshootingLog = theTroubleshootingLog;
129        }
130
131        private void applyRulesAndFailIfDeny(
132                        RestOperationTypeEnum theOperation,
133                        RequestDetails theRequestDetails,
134                        IBaseResource theInputResource,
135                        IIdType theInputResourceId,
136                        IBaseResource theOutputResource,
137                        Pointcut thePointcut) {
138                Verdict decision = applyRulesAndReturnDecision(
139                                theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, thePointcut);
140
141                if (decision.getDecision() == PolicyEnum.ALLOW) {
142                        return;
143                }
144
145                handleDeny(theRequestDetails, decision);
146        }
147
148        @Override
149        public Verdict applyRulesAndReturnDecision(
150                        RestOperationTypeEnum theOperation,
151                        RequestDetails theRequestDetails,
152                        IBaseResource theInputResource,
153                        IIdType theInputResourceId,
154                        IBaseResource theOutputResource,
155                        Pointcut thePointcut) {
156                @SuppressWarnings("unchecked")
157                List<IAuthRule> rules =
158                                (List<IAuthRule>) theRequestDetails.getUserData().get(myRequestRuleListKey);
159                if (rules == null) {
160                        rules = buildRuleList(theRequestDetails);
161                        theRequestDetails.getUserData().put(myRequestRuleListKey, rules);
162                }
163                Set<AuthorizationFlagsEnum> flags = getFlags();
164
165                ourLog.trace(
166                                "Applying {} rules to render an auth decision for operation {}, theInputResource type={}, theOutputResource type={}, thePointcut={} ",
167                                rules.size(),
168                                getPointcutNameOrEmpty(thePointcut),
169                                getResourceTypeOrEmpty(theInputResource),
170                                getResourceTypeOrEmpty(theOutputResource),
171                                thePointcut);
172
173                Verdict verdict = null;
174                for (IAuthRule nextRule : rules) {
175                        ourLog.trace("Rule being applied - {}", nextRule);
176                        verdict = nextRule.applyRule(
177                                        theOperation,
178                                        theRequestDetails,
179                                        theInputResource,
180                                        theInputResourceId,
181                                        theOutputResource,
182                                        this,
183                                        flags,
184                                        thePointcut);
185                        if (verdict != null) {
186                                ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision());
187                                break;
188                        }
189                }
190
191                if (verdict == null) {
192                        ourLog.trace("No rules returned a decision, applying default {}", myDefaultPolicy);
193                        return new Verdict(getDefaultPolicy(), null);
194                }
195
196                return verdict;
197        }
198
199        /**
200         * @since 6.0.0
201         */
202        @Nullable
203        @Override
204        public IValidationSupport getValidationSupport() {
205                return myValidationSupport;
206        }
207
208        /**
209         * Sets a validation support module that will be used for terminology-based rules
210         *
211         * @param theValidationSupport The validation support. Null is also acceptable (this is the default),
212         *                             in which case the validation support module associated with the {@link FhirContext}
213         *                             will be used.
214         * @since 6.0.0
215         */
216        public AuthorizationInterceptor setValidationSupport(IValidationSupport theValidationSupport) {
217                myValidationSupport = theValidationSupport;
218                return this;
219        }
220
221        /**
222         * Sets a search parameter matcher for use in handling SMART v2 filter scopes
223         *
224         * @param theAuthorizationSearchParamMatcher The search parameter matcher. Defaults to null.
225         */
226        public void setAuthorizationSearchParamMatcher(
227                        @Nullable IAuthorizationSearchParamMatcher theAuthorizationSearchParamMatcher) {
228                this.myAuthorizationSearchParamMatcher = theAuthorizationSearchParamMatcher;
229        }
230
231        @Override
232        @Nullable
233        public IAuthorizationSearchParamMatcher getSearchParamMatcher() {
234                return myAuthorizationSearchParamMatcher;
235        }
236
237        /**
238         * Subclasses should override this method to supply the set of rules to be applied to
239         * this individual request.
240         * <p>
241         * Typically this is done by examining <code>theRequestDetails</code> to find
242         * out who the current user is and then using a {@link RuleBuilder} to create
243         * an appropriate rule chain.
244         * </p>
245         *
246         * @param theRequestDetails The individual request currently being applied
247         */
248        public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
249                return new ArrayList<>();
250        }
251
252        private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation) {
253
254                switch (theOperation) {
255                        case ADD_TAGS:
256                        case DELETE_TAGS:
257                        case GET_TAGS:
258                                // These are DSTU1 operations and not relevant
259                                return OperationExamineDirection.NONE;
260
261                        case EXTENDED_OPERATION_INSTANCE:
262                        case EXTENDED_OPERATION_SERVER:
263                        case EXTENDED_OPERATION_TYPE:
264                                return OperationExamineDirection.BOTH;
265
266                        case METADATA:
267                                // Security does not apply to these operations
268                                return OperationExamineDirection.IN;
269
270                        case DELETE:
271                                // Delete is a special case
272                                return OperationExamineDirection.IN;
273
274                        case CREATE:
275                        case UPDATE:
276                        case PATCH:
277                                return OperationExamineDirection.IN;
278
279                        case META:
280                        case META_ADD:
281                        case META_DELETE:
282                                // meta operations do not apply yet
283                                return OperationExamineDirection.NONE;
284
285                        case GET_PAGE:
286                        case HISTORY_INSTANCE:
287                        case HISTORY_SYSTEM:
288                        case HISTORY_TYPE:
289                        case READ:
290                        case SEARCH_SYSTEM:
291                        case SEARCH_TYPE:
292                        case VREAD:
293                                return OperationExamineDirection.OUT;
294
295                        case TRANSACTION:
296                                return OperationExamineDirection.BOTH;
297
298                        case VALIDATE:
299                                // Nothing yet
300                                return OperationExamineDirection.NONE;
301
302                        case GRAPHQL_REQUEST:
303                                return OperationExamineDirection.BOTH;
304
305                        default:
306                                // Should not happen
307                                throw new IllegalStateException(
308                                                Msg.code(332) + "Unable to apply security to event of type " + theOperation);
309                }
310        }
311
312        /**
313         * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY}
314         */
315        public PolicyEnum getDefaultPolicy() {
316                return myDefaultPolicy;
317        }
318
319        /**
320         * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY}
321         *
322         * @param theDefaultPolicy The policy (must not be <code>null</code>)
323         */
324        public AuthorizationInterceptor setDefaultPolicy(PolicyEnum theDefaultPolicy) {
325                Validate.notNull(theDefaultPolicy, "theDefaultPolicy must not be null");
326                myDefaultPolicy = theDefaultPolicy;
327                return this;
328        }
329
330        /**
331         * This property configures any flags affecting how authorization is
332         * applied. By default no flags are applied.
333         *
334         * @see #setFlags(Collection)
335         */
336        public Set<AuthorizationFlagsEnum> getFlags() {
337                return Collections.unmodifiableSet(myFlags);
338        }
339
340        /**
341         * This property configures any flags affecting how authorization is
342         * applied. By default no flags are applied.
343         *
344         * @param theFlags The flags (must not be null)
345         * @see #setFlags(AuthorizationFlagsEnum...)
346         */
347        public AuthorizationInterceptor setFlags(Collection<AuthorizationFlagsEnum> theFlags) {
348                Validate.notNull(theFlags, "theFlags must not be null");
349                myFlags = new HashSet<>(theFlags);
350                return this;
351        }
352
353        /**
354         * This property configures any flags affecting how authorization is
355         * applied. By default no flags are applied.
356         *
357         * @param theFlags The flags (must not be null)
358         * @see #setFlags(Collection)
359         */
360        public AuthorizationInterceptor setFlags(AuthorizationFlagsEnum... theFlags) {
361                Validate.notNull(theFlags, "theFlags must not be null");
362                return setFlags(Lists.newArrayList(theFlags));
363        }
364
365        /**
366         * Handle an access control verdict of {@link PolicyEnum#DENY}.
367         * <p>
368         * Subclasses may override to implement specific behaviour, but default is to
369         * throw {@link ForbiddenOperationException} (HTTP 403) with error message citing the
370         * rule name which trigered failure
371         * </p>
372         *
373         * @since HAPI FHIR 3.6.0
374         */
375        protected void handleDeny(RequestDetails theRequestDetails, Verdict decision) {
376                handleDeny(decision);
377        }
378
379        /**
380         * This method should not be overridden. As of HAPI FHIR 3.6.0, you
381         * should override {@link #handleDeny(RequestDetails, Verdict)} instead. This
382         * method will be removed in the future.
383         */
384        protected void handleDeny(Verdict decision) {
385                if (decision.getDecidingRule() != null) {
386                        String ruleName = Objects.toString(decision.getDecidingRule().getName(), "(unnamed rule)");
387                        throw new ForbiddenOperationException(Msg.code(333) + "Access denied by rule: " + ruleName);
388                }
389                throw new ForbiddenOperationException(Msg.code(334) + "Access denied by default policy (no applicable rules)");
390        }
391
392        private void handleUserOperation(
393                        RequestDetails theRequest,
394                        IBaseResource theResource,
395                        RestOperationTypeEnum theOperation,
396                        Pointcut thePointcut) {
397                applyRulesAndFailIfDeny(theOperation, theRequest, theResource, theResource.getIdElement(), null, thePointcut);
398        }
399
400        @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
401        public void incomingRequestPreHandled(RequestDetails theRequest, Pointcut thePointcut) {
402                IBaseResource inputResource = null;
403                IIdType inputResourceId = null;
404
405                switch (determineOperationDirection(theRequest.getRestOperationType())) {
406                        case IN:
407                        case BOTH:
408                                inputResource = theRequest.getResource();
409                                inputResourceId = theRequest.getId();
410                                if (inputResourceId == null && isNotBlank(theRequest.getResourceName())) {
411                                        inputResourceId = theRequest.getFhirContext().getVersion().newIdType();
412                                        inputResourceId.setParts(null, theRequest.getResourceName(), null, null);
413                                }
414                                break;
415                        case OUT:
416                                // inputResource = null;
417                                inputResourceId = theRequest.getId();
418                                break;
419                        case NONE:
420                                return;
421                }
422
423                applyRulesAndFailIfDeny(
424                                theRequest.getRestOperationType(), theRequest, inputResource, inputResourceId, null, thePointcut);
425        }
426
427        @Hook(Pointcut.STORAGE_PRESHOW_RESOURCES)
428        public void hookPreShow(
429                        RequestDetails theRequestDetails, IPreResourceShowDetails theDetails, Pointcut thePointcut) {
430                for (int i = 0; i < theDetails.size(); i++) {
431                        IBaseResource next = theDetails.getResource(i);
432                        checkOutgoingResourceAndFailIfDeny(theRequestDetails, next, thePointcut);
433                }
434        }
435
436        @Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
437        public void hookOutgoingResponse(
438                        RequestDetails theRequestDetails, IBaseResource theResponseObject, Pointcut thePointcut) {
439                checkOutgoingResourceAndFailIfDeny(theRequestDetails, theResponseObject, thePointcut);
440        }
441
442        @Hook(Pointcut.STORAGE_CASCADE_DELETE)
443        public void hookCascadeDeleteForConflict(
444                        RequestDetails theRequestDetails, Pointcut thePointcut, IBaseResource theResourceToDelete) {
445                Objects.requireNonNull(theResourceToDelete); // just in case
446                checkPointcutAndFailIfDeny(theRequestDetails, thePointcut, theResourceToDelete);
447        }
448
449        @Hook(Pointcut.STORAGE_PRE_DELETE_EXPUNGE)
450        public void hookDeleteExpunge(RequestDetails theRequestDetails, Pointcut thePointcut) {
451                applyRulesAndFailIfDeny(
452                                theRequestDetails.getRestOperationType(), theRequestDetails, null, null, null, thePointcut);
453        }
454
455        @Hook(Pointcut.STORAGE_INITIATE_BULK_EXPORT)
456        public void initiateBulkExport(
457                        RequestDetails theRequestDetails, BulkExportJobParameters theBulkExportOptions, Pointcut thePointcut) {
458                //              RestOperationTypeEnum restOperationType =
459                // determineRestOperationTypeFromBulkExportOptions(theBulkExportOptions);
460                RestOperationTypeEnum restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
461
462                if (theRequestDetails != null) {
463                        theRequestDetails.getUserData().put(REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS, theBulkExportOptions);
464                }
465                applyRulesAndFailIfDeny(restOperationType, theRequestDetails, null, null, null, thePointcut);
466        }
467
468        /**
469         * TODO GGG This method should eventually be used when invoking the rules applier.....however we currently rely on the incorrect
470         * behaviour of passing down `EXTENDED_OPERATION_SERVER`.
471         */
472        private RestOperationTypeEnum determineRestOperationTypeFromBulkExportOptions(
473                        BulkExportJobParameters theBulkExportOptions) {
474                RestOperationTypeEnum restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
475                BulkExportJobParameters.ExportStyle exportStyle = theBulkExportOptions.getExportStyle();
476                if (exportStyle.equals(BulkExportJobParameters.ExportStyle.PATIENT)) {
477                        if (theBulkExportOptions.getPatientIds().size() == 1) {
478                                restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
479                        } else {
480                                restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
481                        }
482                } else if (exportStyle.equals(BulkExportJobParameters.ExportStyle.GROUP)) {
483                        restOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
484                }
485                return restOperationType;
486        }
487
488        private void checkPointcutAndFailIfDeny(
489                        RequestDetails theRequestDetails, Pointcut thePointcut, @Nonnull IBaseResource theInputResource) {
490                applyRulesAndFailIfDeny(
491                                theRequestDetails.getRestOperationType(),
492                                theRequestDetails,
493                                theInputResource,
494                                theInputResource.getIdElement(),
495                                null,
496                                thePointcut);
497        }
498
499        private void checkOutgoingResourceAndFailIfDeny(
500                        RequestDetails theRequestDetails, IBaseResource theResponseObject, Pointcut thePointcut) {
501
502                switch (determineOperationDirection(theRequestDetails.getRestOperationType())) {
503                        case IN:
504                        case NONE:
505                                return;
506                        case BOTH:
507                        case OUT:
508                                break;
509                }
510
511                // Don't check the value twice
512                IdentityHashMap<IBaseResource, Boolean> alreadySeenMap = getAlreadySeenResourcesMap(theRequestDetails);
513                if (alreadySeenMap.putIfAbsent(theResponseObject, Boolean.TRUE) != null) {
514                        return;
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(
533                                                        theResponseObject, fhirContext, theRequestDetails);
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> toListOfResourcesAndExcludeContainerUnlessStandalone(
581                        IBaseResource theResponseObject, FhirContext fhirContext, RequestDetails theRequestDetails) {
582
583                if (theResponseObject == null) {
584                        return Collections.emptyList();
585                }
586
587                boolean shouldExamineChildResources = shouldExamineChildResources(theResponseObject, fhirContext);
588                if (!shouldExamineChildResources) {
589                        return toListOfResourcesAndExcludeOperationOutcomeBasedOnRestOperationType(
590                                        theResponseObject, theRequestDetails);
591                }
592
593                return toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext);
594        }
595
596        /**
597         *
598         * @param theResponseObject The resource to convert to a list.
599         * @param theRequestDetails The request details.
600         * @return The response object (a resource) as a list. If the REST operation type in the request details is a
601         * search, and the search is for resources that aren't the OperationOutcome, any OperationOutcome resource is removed from the list.
602         * e.g. A GET [base]/Patient?parameter(s) search may return a bundle containing an OperationOutcome. The OperationOutcome will be removed from the
603         * list to exclude from security.
604         */
605        private static List<IBaseResource> toListOfResourcesAndExcludeOperationOutcomeBasedOnRestOperationType(
606                        IBaseResource theResponseObject, RequestDetails theRequestDetails) {
607                List<IBaseResource> resources = new ArrayList<>();
608                RestOperationTypeEnum restOperationType = theRequestDetails.getRestOperationType();
609                String resourceName = theRequestDetails.getResourceName();
610                resources.add(theResponseObject);
611
612                if (resourceName != null
613                                && !resourceName.equals("OperationOutcome")
614                                && REST_OPERATIONS_TO_EXCLUDE_SECURITY_FOR_OPERATION_OUTCOME.contains(restOperationType)) {
615                        resources.removeIf(t -> t instanceof IBaseOperationOutcome);
616                }
617
618                return resources;
619        }
620
621        @Nonnull
622        public static List<IBaseResource> toListOfResourcesAndExcludeContainer(
623                        IBaseResource theResponseObject, FhirContext fhirContext) {
624                List<IBaseResource> retVal;
625                retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class);
626
627                // Exclude the container
628                if (!retVal.isEmpty() && retVal.get(0) == theResponseObject) {
629                        retVal = retVal.subList(1, retVal.size());
630                }
631
632                // Don't apply security to OperationOutcome
633                retVal.removeIf(t -> t instanceof IBaseOperationOutcome);
634
635                return retVal;
636        }
637
638        /**
639         * This method determines if the given Resource should have permissions applied
640         * to the resources inside or to the Resource itself.
641         * For Parameters resources, we include child resources when checking the permissions.
642         * For Bundle resources, we look at resources inside if the Bundle type is not in
643         * STANDALONE_BUNDLE_RESOURCE_TYPES set.
644         */
645        protected static boolean shouldExamineChildResources(IBaseResource theResource, FhirContext theFhirContext) {
646                if (theResource instanceof IBaseParameters) {
647                        return true;
648                }
649
650                if (theResource instanceof IBaseBundle baseBundle) {
651                        BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, baseBundle);
652                        boolean isStandaloneBundleResource =
653                                        bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType);
654                        return !isStandaloneBundleResource;
655                }
656
657                return false;
658        }
659
660        public static class Verdict {
661
662                private final IAuthRule myDecidingRule;
663                private final PolicyEnum myDecision;
664
665                public Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) {
666                        Objects.requireNonNull(theDecision);
667
668                        myDecision = theDecision;
669                        myDecidingRule = theDecidingRule;
670                }
671
672                IAuthRule getDecidingRule() {
673                        return myDecidingRule;
674                }
675
676                public PolicyEnum getDecision() {
677                        return myDecision;
678                }
679
680                @Override
681                public String toString() {
682                        ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
683                        String ruleName;
684                        if (myDecidingRule != null) {
685                                ruleName = myDecidingRule.getName();
686                        } else {
687                                ruleName = "(none)";
688                        }
689                        b.append("rule", ruleName);
690                        b.append("decision", myDecision.name());
691                        return b.build();
692                }
693        }
694
695        private Object getPointcutNameOrEmpty(Pointcut thePointcut) {
696                return nonNull(thePointcut) ? thePointcut.name() : EMPTY;
697        }
698
699        private String getResourceTypeOrEmpty(IBaseResource theResource) {
700                String retVal = StringUtils.EMPTY;
701
702                if (isNull(theResource)) {
703                        return retVal;
704                }
705
706                if (isNull(theResource.getIdElement())) {
707                        return retVal;
708                }
709
710                if (isNull(theResource.getIdElement().getResourceType())) {
711                        return retVal;
712                }
713
714                return theResource.getIdElement().getResourceType();
715        }
716
717        @SuppressWarnings("unchecked")
718        private IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails) {
719                IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>)
720                                theRequestDetails.getUserData().get(myRequestSeenResourcesKey);
721                if (alreadySeenResources == null) {
722                        alreadySeenResources = new IdentityHashMap<>();
723                        theRequestDetails.getUserData().put(myRequestSeenResourcesKey, alreadySeenResources);
724                }
725                return alreadySeenResources;
726        }
727}