001package ca.uhn.fhir.rest.server.interceptor.auth;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
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.interceptor.api.Hook;
025import ca.uhn.fhir.interceptor.api.Interceptor;
026import ca.uhn.fhir.interceptor.api.Pointcut;
027import ca.uhn.fhir.model.api.TagList;
028import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
029import ca.uhn.fhir.rest.api.server.RequestDetails;
030import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
031import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
032import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
033import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
034import ca.uhn.fhir.util.CoverageIgnore;
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.servlet.http.HttpServletRequest;
047import javax.servlet.http.HttpServletResponse;
048import java.util.*;
049
050import static org.apache.commons.lang3.StringUtils.defaultString;
051
052/**
053 * This class is a base class for interceptors which can be used to
054 * inspect requests and responses to determine whether the calling user
055 * has permission to perform the given action.
056 * <p>
057 * See the HAPI FHIR
058 * <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_server_security.html">Documentation on Server Security</a>
059 * for information on how to use this interceptor.
060 * </p>
061 *
062 * @see SearchNarrowingInterceptor
063 */
064@Interceptor
065public class AuthorizationInterceptor implements IRuleApplier {
066
067        private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class);
068
069        private PolicyEnum myDefaultPolicy = PolicyEnum.DENY;
070        private Set<AuthorizationFlagsEnum> myFlags = Collections.emptySet();
071
072        /**
073         * Constructor
074         */
075        public AuthorizationInterceptor() {
076                super();
077        }
078
079        /**
080         * Constructor
081         *
082         * @param theDefaultPolicy The default policy if no rules apply (must not be null)
083         */
084        public AuthorizationInterceptor(PolicyEnum theDefaultPolicy) {
085                this();
086                setDefaultPolicy(theDefaultPolicy);
087        }
088
089        private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId,
090                                                                                                         IBaseResource theOutputResource) {
091                Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
092
093                if (decision.getDecision() == PolicyEnum.ALLOW) {
094                        return;
095                }
096
097                handleDeny(theRequestDetails, decision);
098        }
099
100        @Override
101        public Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId,
102                                                                                                                         IBaseResource theOutputResource) {
103                List<IAuthRule> rules = buildRuleList(theRequestDetails);
104                Set<AuthorizationFlagsEnum> flags = getFlags();
105                ourLog.trace("Applying {} rules to render an auth decision for operation {}", rules.size(), theOperation);
106
107                Verdict verdict = null;
108                for (IAuthRule nextRule : rules) {
109                        verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, this, flags);
110                        if (verdict != null) {
111                                ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision());
112                                break;
113                        }
114                }
115
116                if (verdict == null) {
117                        ourLog.trace("No rules returned a decision, applying default {}", myDefaultPolicy);
118                        return new Verdict(getDefaultPolicy(), null);
119                }
120
121                return verdict;
122        }
123
124        /**
125         * Subclasses should override this method to supply the set of rules to be applied to
126         * this individual request.
127         * <p>
128         * Typically this is done by examining <code>theRequestDetails</code> to find
129         * out who the current user is and then using a {@link RuleBuilder} to create
130         * an appropriate rule chain.
131         * </p>
132         *
133         * @param theRequestDetails The individual request currently being applied
134         */
135        public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
136                return new ArrayList<>();
137        }
138
139        private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation, IBaseResource theRequestResource) {
140                switch (theOperation) {
141                        case ADD_TAGS:
142                        case DELETE_TAGS:
143                        case GET_TAGS:
144                                // These are DSTU1 operations and not relevant
145                                return OperationExamineDirection.NONE;
146
147                        case EXTENDED_OPERATION_INSTANCE:
148                        case EXTENDED_OPERATION_SERVER:
149                        case EXTENDED_OPERATION_TYPE:
150                                return OperationExamineDirection.BOTH;
151
152                        case METADATA:
153                                // Security does not apply to these operations
154                                return OperationExamineDirection.IN;
155
156                        case DELETE:
157                                // Delete is a special case
158                                return OperationExamineDirection.NONE;
159
160                        case CREATE:
161                        case UPDATE:
162                        case PATCH:
163                                // if (theRequestResource != null) {
164                                // if (theRequestResource.getIdElement() != null) {
165                                // if (theRequestResource.getIdElement().hasIdPart() == false) {
166                                // return OperationExamineDirection.IN_UNCATEGORIZED;
167                                // }
168                                // }
169                                // }
170                                return OperationExamineDirection.IN;
171
172                        case META:
173                        case META_ADD:
174                        case META_DELETE:
175                                // meta operations do not apply yet
176                                return OperationExamineDirection.NONE;
177
178                        case GET_PAGE:
179                        case HISTORY_INSTANCE:
180                        case HISTORY_SYSTEM:
181                        case HISTORY_TYPE:
182                        case READ:
183                        case SEARCH_SYSTEM:
184                        case SEARCH_TYPE:
185                        case VREAD:
186                                return OperationExamineDirection.OUT;
187
188                        case TRANSACTION:
189                                return OperationExamineDirection.BOTH;
190
191                        case VALIDATE:
192                                // Nothing yet
193                                return OperationExamineDirection.NONE;
194
195                        case GRAPHQL_REQUEST:
196                                return OperationExamineDirection.IN;
197
198                        default:
199                                // Should not happen
200                                throw new IllegalStateException("Unable to apply security to event of type " + theOperation);
201                }
202
203        }
204
205        /**
206         * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY}
207         */
208        public PolicyEnum getDefaultPolicy() {
209                return myDefaultPolicy;
210        }
211
212        /**
213         * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY}
214         *
215         * @param theDefaultPolicy The policy (must not be <code>null</code>)
216         */
217        public void setDefaultPolicy(PolicyEnum theDefaultPolicy) {
218                Validate.notNull(theDefaultPolicy, "theDefaultPolicy must not be null");
219                myDefaultPolicy = theDefaultPolicy;
220        }
221
222        /**
223         * This property configures any flags affecting how authorization is
224         * applied. By default no flags are applied.
225         *
226         * @see #setFlags(Collection)
227         */
228        public Set<AuthorizationFlagsEnum> getFlags() {
229                return Collections.unmodifiableSet(myFlags);
230        }
231
232        /**
233         * This property configures any flags affecting how authorization is
234         * applied. By default no flags are applied.
235         *
236         * @param theFlags The flags (must not be null)
237         * @see #setFlags(AuthorizationFlagsEnum...)
238         */
239        public AuthorizationInterceptor setFlags(Collection<AuthorizationFlagsEnum> theFlags) {
240                Validate.notNull(theFlags, "theFlags must not be null");
241                myFlags = new HashSet<>(theFlags);
242                return this;
243        }
244
245        /**
246         * This property configures any flags affecting how authorization is
247         * applied. By default no flags are applied.
248         *
249         * @param theFlags The flags (must not be null)
250         * @see #setFlags(Collection)
251         */
252        public AuthorizationInterceptor setFlags(AuthorizationFlagsEnum... theFlags) {
253                Validate.notNull(theFlags, "theFlags must not be null");
254                return setFlags(Lists.newArrayList(theFlags));
255        }
256
257        /**
258         * Handle an access control verdict of {@link PolicyEnum#DENY}.
259         * <p>
260         * Subclasses may override to implement specific behaviour, but default is to
261         * throw {@link ForbiddenOperationException} (HTTP 403) with error message citing the
262         * rule name which trigered failure
263         * </p>
264         *
265         * @since HAPI FHIR 3.6.0
266         */
267        protected void handleDeny(RequestDetails theRequestDetails, Verdict decision) {
268                handleDeny(decision);
269        }
270
271        /**
272         * This method should not be overridden. As of HAPI FHIR 3.6.0, you
273         * should override {@link #handleDeny(RequestDetails, Verdict)} instead. This
274         * method will be removed in the future.
275         */
276        protected void handleDeny(Verdict decision) {
277                if (decision.getDecidingRule() != null) {
278                        String ruleName = defaultString(decision.getDecidingRule().getName(), "(unnamed rule)");
279                        throw new ForbiddenOperationException("Access denied by rule: " + ruleName);
280                }
281                throw new ForbiddenOperationException("Access denied by default policy (no applicable rules)");
282        }
283
284        private void handleUserOperation(RequestDetails theRequest, IBaseResource theResource, RestOperationTypeEnum operation) {
285                applyRulesAndFailIfDeny(operation, theRequest, theResource, theResource.getIdElement(), null);
286        }
287
288        @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
289        public void incomingRequestPreHandled(RestOperationTypeEnum theOperation, IServerInterceptor.ActionRequestDetails theProcessedRequest) {
290                IBaseResource inputResource = null;
291                IIdType inputResourceId = null;
292
293                switch (determineOperationDirection(theOperation, theProcessedRequest.getResource())) {
294                        case IN:
295                        case BOTH:
296                                inputResource = theProcessedRequest.getResource();
297                                inputResourceId = theProcessedRequest.getId();
298                                break;
299                        case OUT:
300                                // inputResource = null;
301                                inputResourceId = theProcessedRequest.getId();
302                                break;
303                        case NONE:
304                                return;
305                }
306
307                RequestDetails requestDetails = theProcessedRequest.getRequestDetails();
308                applyRulesAndFailIfDeny(theOperation, requestDetails, inputResource, inputResourceId, null);
309        }
310
311        @Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
312        public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
313                switch (determineOperationDirection(theRequestDetails.getRestOperationType(), null)) {
314                        case IN:
315                        case NONE:
316                                return true;
317                        case BOTH:
318                        case OUT:
319                                break;
320                }
321
322                FhirContext fhirContext = theRequestDetails.getServer().getFhirContext();
323                List<IBaseResource> resources = Collections.emptyList();
324
325                switch (theRequestDetails.getRestOperationType()) {
326                        case SEARCH_SYSTEM:
327                        case SEARCH_TYPE:
328                        case HISTORY_INSTANCE:
329                        case HISTORY_SYSTEM:
330                        case HISTORY_TYPE:
331                        case TRANSACTION:
332                        case GET_PAGE:
333                        case EXTENDED_OPERATION_SERVER:
334                        case EXTENDED_OPERATION_TYPE:
335                        case EXTENDED_OPERATION_INSTANCE: {
336                                if (theResponseObject != null) {
337                                        resources = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext);
338                                }
339                                break;
340                        }
341                        default: {
342                                if (theResponseObject != null) {
343                                        resources = Collections.singletonList(theResponseObject);
344                                }
345                                break;
346                        }
347                }
348
349                for (IBaseResource nextResponse : resources) {
350                        applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, null, nextResponse);
351                }
352
353                return true;
354        }
355
356        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
357        public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) {
358                handleUserOperation(theRequest, theResource, RestOperationTypeEnum.CREATE);
359        }
360
361        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED)
362        public void resourcePreDelete(RequestDetails theRequest, IBaseResource theResource) {
363                handleUserOperation(theRequest, theResource, RestOperationTypeEnum.DELETE);
364        }
365
366        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
367        public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
368                if (theOldResource != null) {
369                        handleUserOperation(theRequest, theOldResource, RestOperationTypeEnum.UPDATE);
370                }
371                handleUserOperation(theRequest, theNewResource, RestOperationTypeEnum.UPDATE);
372        }
373
374        private enum OperationExamineDirection {
375                BOTH,
376                IN,
377                NONE,
378                OUT,
379        }
380
381        public static class Verdict {
382
383                private final IAuthRule myDecidingRule;
384                private final PolicyEnum myDecision;
385
386                Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) {
387                        Validate.notNull(theDecision);
388
389                        myDecision = theDecision;
390                        myDecidingRule = theDecidingRule;
391                }
392
393                public IAuthRule getDecidingRule() {
394                        return myDecidingRule;
395                }
396
397                public PolicyEnum getDecision() {
398                        return myDecision;
399                }
400
401                @Override
402                public String toString() {
403                        ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
404                        String ruleName;
405                        if (myDecidingRule != null) {
406                                ruleName = myDecidingRule.getName();
407                        } else {
408                                ruleName = "(none)";
409                        }
410                        b.append("rule", ruleName);
411                        b.append("decision", myDecision.name());
412                        return b.build();
413                }
414
415        }
416
417        private static UnsupportedOperationException failForDstu1() {
418                return new UnsupportedOperationException("Use of this interceptor on DSTU1 servers is not supportd");
419        }
420
421        static List<IBaseResource> toListOfResourcesAndExcludeContainer(IBaseResource theResponseObject, FhirContext fhirContext) {
422                if (theResponseObject == null) {
423                        return Collections.emptyList();
424                }
425
426                List<IBaseResource> retVal;
427
428                boolean isContainer = false;
429                if (theResponseObject instanceof IBaseBundle) {
430                        isContainer = true;
431                } else if (theResponseObject instanceof IBaseParameters) {
432                        isContainer = true;
433                }
434
435                if (!isContainer) {
436                        return Collections.singletonList(theResponseObject);
437                }
438
439                retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class);
440
441                // Exclude the container
442                if (retVal.size() > 0 && retVal.get(0) == theResponseObject) {
443                        retVal = retVal.subList(1, retVal.size());
444                }
445
446                return retVal;
447        }
448
449}