package ca.uhn.fhir.rest.server.interceptor.auth; /* * #%L * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2017 University Health Network * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import static org.apache.commons.lang3.StringUtils.defaultString; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; import ca.uhn.fhir.util.CoverageIgnore; /** * This class is a base class for interceptors which can be used to * inspect requests and responses to determine whether the calling user * has permission to perform the given action. * <p> * See the HAPI FHIR * <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_server_security.html">Documentation on Server Security</a> * for information on how to use this interceptor. * </p> */ public class AuthorizationInterceptor extends InterceptorAdapter implements IServerOperationInterceptor, IRuleApplier { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AuthorizationInterceptor.class); private PolicyEnum myDefaultPolicy = PolicyEnum.DENY; /** * Constructor */ public AuthorizationInterceptor() { super(); } /** * Constructor * * @param theDefaultPolicy * The default policy if no rules apply (must not be null) */ public AuthorizationInterceptor(PolicyEnum theDefaultPolicy) { this(); setDefaultPolicy(theDefaultPolicy); } private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) { Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); if (decision.getDecision() == PolicyEnum.ALLOW) { return; } handleDeny(decision); } @Override public Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) { List<IAuthRule> rules = buildRuleList(theRequestDetails); ourLog.trace("Applying {} rules to render an auth decision for operation {}", rules.size(), theOperation); Verdict verdict = null; for (IAuthRule nextRule : rules) { verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, this); if (verdict != null) { ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision()); break; } } if (verdict == null) { ourLog.trace("No rules returned a decision, applying default {}", myDefaultPolicy); return new Verdict(myDefaultPolicy, null); } return verdict; } /** * Subclasses should override this method to supply the set of rules to be applied to * this individual request. * <p> * Typically this is done by examining <code>theRequestDetails</code> to find * out who the current user is and then using a {@link RuleBuilder} to create * an appropriate rule chain. * </p> * * @param theRequestDetails * The individual request currently being applied */ public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { return new ArrayList<IAuthRule>(); } private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation, IBaseResource theRequestResource) { switch (theOperation) { case ADD_TAGS: case DELETE_TAGS: case GET_TAGS: // These are DSTU1 operations and not relevant return OperationExamineDirection.NONE; case EXTENDED_OPERATION_INSTANCE: case EXTENDED_OPERATION_SERVER: case EXTENDED_OPERATION_TYPE: return OperationExamineDirection.BOTH; case METADATA: // Security does not apply to these operations return OperationExamineDirection.IN; case DELETE: // Delete is a special case return OperationExamineDirection.NONE; case CREATE: case UPDATE: // if (theRequestResource != null) { // if (theRequestResource.getIdElement() != null) { // if (theRequestResource.getIdElement().hasIdPart() == false) { // return OperationExamineDirection.IN_UNCATEGORIZED; // } // } // } return OperationExamineDirection.IN; case META: case META_ADD: case META_DELETE: // meta operations do not apply yet return OperationExamineDirection.NONE; case GET_PAGE: case HISTORY_INSTANCE: case HISTORY_SYSTEM: case HISTORY_TYPE: case READ: case SEARCH_SYSTEM: case SEARCH_TYPE: case VREAD: return OperationExamineDirection.OUT; case TRANSACTION: return OperationExamineDirection.BOTH; case VALIDATE: // Nothing yet return OperationExamineDirection.NONE; default: // Should not happen throw new IllegalStateException("Unable to apply security to event of type " + theOperation); } } /** * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY} */ public PolicyEnum getDefaultPolicy() { return myDefaultPolicy; } /** * Handle an access control verdict of {@link PolicyEnum#DENY}. * <p> * Subclasses may override to implement specific behaviour, but default is to * throw {@link ForbiddenOperationException} (HTTP 403) with error message citing the * rule name which trigered failure * </p> */ protected void handleDeny(Verdict decision) { if (decision.getDecidingRule() != null) { String ruleName = defaultString(decision.getDecidingRule().getName(), "(unnamed rule)"); throw new ForbiddenOperationException("Access denied by rule: " + ruleName); } throw new ForbiddenOperationException("Access denied by default policy (no applicable rules)"); } private void handleUserOperation(RequestDetails theRequest, IBaseResource theResource, RestOperationTypeEnum operation) { applyRulesAndFailIfDeny(operation, theRequest, theResource, theResource.getIdElement(), null); } @Override public void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theProcessedRequest) { IBaseResource inputResource = null; IIdType inputResourceId = null; switch (determineOperationDirection(theOperation, theProcessedRequest.getResource())) { case IN: case BOTH: inputResource = theProcessedRequest.getResource(); inputResourceId = theProcessedRequest.getId(); break; case OUT: // inputResource = null; inputResourceId = theProcessedRequest.getId(); break; case NONE: return; } RequestDetails requestDetails = theProcessedRequest.getRequestDetails(); applyRulesAndFailIfDeny(theOperation, requestDetails, inputResource, inputResourceId, null); } @Override @CoverageIgnore public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theBundle) { throw failForDstu1(); } @Override @CoverageIgnore public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { throw failForDstu1(); } @Override public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { switch (determineOperationDirection(theRequestDetails.getRestOperationType(), null)) { case IN: case NONE: return true; case BOTH: case OUT: break; } FhirContext fhirContext = theRequestDetails.getServer().getFhirContext(); List<IBaseResource> resources = Collections.emptyList(); switch (theRequestDetails.getRestOperationType()) { case SEARCH_SYSTEM: case SEARCH_TYPE: case HISTORY_INSTANCE: case HISTORY_SYSTEM: case HISTORY_TYPE: case TRANSACTION: case GET_PAGE: case EXTENDED_OPERATION_SERVER: case EXTENDED_OPERATION_TYPE: case EXTENDED_OPERATION_INSTANCE: { if (theResponseObject != null) { if (theResponseObject instanceof IBaseBundle) { resources = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext); } else if (theResponseObject instanceof IBaseParameters) { resources = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext); } } break; } default: { if (theResponseObject != null) { resources = Collections.singletonList(theResponseObject); } break; } } for (IBaseResource nextResponse : resources) { applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, null, nextResponse); } return true; } @CoverageIgnore @Override public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject) { throw failForDstu1(); } @CoverageIgnore @Override public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { throw failForDstu1(); } @Override public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { handleUserOperation(theRequest, theResource, RestOperationTypeEnum.CREATE); } @Override public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { handleUserOperation(theRequest, theResource, RestOperationTypeEnum.DELETE); } @Override public void resourceUpdated(RequestDetails theRequest, IBaseResource theResource) { handleUserOperation(theRequest, theResource, RestOperationTypeEnum.UPDATE); } /** * The default policy if no rules have been found to apply. Default value for this setting is {@link PolicyEnum#DENY} * * @param theDefaultPolicy * The policy (must not be <code>null</code>) */ public void setDefaultPolicy(PolicyEnum theDefaultPolicy) { Validate.notNull(theDefaultPolicy, "theDefaultPolicy must not be null"); myDefaultPolicy = theDefaultPolicy; } private List<IBaseResource> toListOfResourcesAndExcludeContainer(IBaseResource theResponseObject, FhirContext fhirContext) { List<IBaseResource> resources; resources = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); // Exclude the container if (resources.size() > 0 && resources.get(0) == theResponseObject) { resources = resources.subList(1, resources.size()); } return resources; } // private List<IBaseResource> toListOfResources(FhirContext fhirContext, IBaseBundle responseBundle) { // List<IBaseResource> retVal = BundleUtil.toListOfResources(fhirContext, responseBundle); // for (int i = 0; i < retVal.size(); i++) { // IBaseResource nextResource = retVal.get(i); // if (nextResource instanceof IBaseBundle) { // retVal.addAll(BundleUtil.toListOfResources(fhirContext, (IBaseBundle) nextResource)); // retVal.remove(i); // i--; // } // } // return retVal; // } private static UnsupportedOperationException failForDstu1() { return new UnsupportedOperationException("Use of this interceptor on DSTU1 servers is not supportd"); } private enum OperationExamineDirection { BOTH, IN, NONE, OUT, } public static class Verdict { private final IAuthRule myDecidingRule; private final PolicyEnum myDecision; public Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) { myDecision = theDecision; myDecidingRule = theDecidingRule; } public IAuthRule getDecidingRule() { return myDecidingRule; } public PolicyEnum getDecision() { return myDecision; } @Override public String toString() { ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); b.append("rule", myDecidingRule.getName()); b.append("decision", myDecision.name()); return b.build(); } } }