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}