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}