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