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