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