001package ca.uhn.fhir.rest.server.interceptor.consent; 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.BaseRuntimeChildDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.interceptor.api.Hook; 027import ca.uhn.fhir.interceptor.api.Interceptor; 028import ca.uhn.fhir.interceptor.api.Pointcut; 029import ca.uhn.fhir.rest.api.Constants; 030import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 031import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.api.server.ResponseDetails; 034import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 035import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; 036import ca.uhn.fhir.rest.server.util.ICachedSearchDetails; 037import ca.uhn.fhir.util.BundleUtil; 038import ca.uhn.fhir.util.IModelVisitor2; 039import org.apache.commons.lang3.Validate; 040import org.hl7.fhir.instance.model.api.*; 041 042import java.util.IdentityHashMap; 043import java.util.List; 044import java.util.Map; 045import java.util.concurrent.atomic.AtomicInteger; 046 047@Interceptor 048public class ConsentInterceptor { 049 private static final AtomicInteger ourInstanceCount = new AtomicInteger(0); 050 private final int myInstanceIndex = ourInstanceCount.incrementAndGet(); 051 private final String myRequestAuthorizedKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_AUTHORIZED"; 052 private final String myRequestCompletedKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_COMPLETED"; 053 private final String myRequestSeenResourcesKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES"; 054 055 private IConsentService myConsentService; 056 private IConsentContextServices myContextConsentServices; 057 058 /** 059 * Constructor 060 */ 061 public ConsentInterceptor() { 062 super(); 063 } 064 065 /** 066 * Constructor 067 * 068 * @param theConsentService Must not be <code>null</code> 069 */ 070 public ConsentInterceptor(IConsentService theConsentService) { 071 this(theConsentService, IConsentContextServices.NULL_IMPL); 072 } 073 074 /** 075 * Constructor 076 * 077 * @param theConsentService Must not be <code>null</code> 078 * @param theContextConsentServices Must not be <code>null</code> 079 */ 080 public ConsentInterceptor(IConsentService theConsentService, IConsentContextServices theContextConsentServices) { 081 setConsentService(theConsentService); 082 setContextConsentServices(theContextConsentServices); 083 } 084 085 public void setContextConsentServices(IConsentContextServices theContextConsentServices) { 086 Validate.notNull(theContextConsentServices, "theContextConsentServices must not be null"); 087 myContextConsentServices = theContextConsentServices; 088 } 089 090 public void setConsentService(IConsentService theConsentService) { 091 Validate.notNull(theConsentService, "theConsentService must not be null"); 092 myConsentService = theConsentService; 093 } 094 095 @Hook(value = Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED) 096 public void interceptPreHandled(RequestDetails theRequestDetails) { 097 ConsentOutcome outcome = myConsentService.startOperation(theRequestDetails, myContextConsentServices); 098 Validate.notNull(outcome, "Consent service returned null outcome"); 099 100 switch (outcome.getStatus()) { 101 case REJECT: 102 throw toForbiddenOperationException(outcome); 103 case PROCEED: 104 break; 105 case AUTHORIZED: 106 Map<Object, Object> userData = theRequestDetails.getUserData(); 107 userData.put(myRequestAuthorizedKey, Boolean.TRUE); 108 break; 109 } 110 } 111 112 @Hook(value = Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH) 113 public boolean interceptPreCheckForCachedSearch(RequestDetails theRequestDetails) { 114 if (isRequestAuthorized(theRequestDetails)) { 115 return true; 116 } 117 return false; 118 } 119 120 @Hook(value = Pointcut.STORAGE_PRESEARCH_REGISTERED) 121 public void interceptPreSearchRegistered(RequestDetails theRequestDetails, ICachedSearchDetails theCachedSearchDetails) { 122 if (!isRequestAuthorized(theRequestDetails)) { 123 theCachedSearchDetails.setCannotBeReused(); 124 } 125 } 126 127 @Hook(value = Pointcut.STORAGE_PREACCESS_RESOURCES) 128 public void interceptPreAccess(RequestDetails theRequestDetails, IPreResourceAccessDetails thePreResourceAccessDetails) { 129 if (isRequestAuthorized(theRequestDetails)) { 130 return; 131 } 132 133 for (int i = 0; i < thePreResourceAccessDetails.size(); i++) { 134 IBaseResource nextResource = thePreResourceAccessDetails.getResource(i); 135 ConsentOutcome nextOutcome = myConsentService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices); 136 switch (nextOutcome.getStatus()) { 137 case PROCEED: 138 break; 139 case AUTHORIZED: 140 break; 141 case REJECT: 142 thePreResourceAccessDetails.setDontReturnResourceAtIndex(i); 143 break; 144 } 145 } 146 } 147 148 @Hook(value = Pointcut.STORAGE_PRESHOW_RESOURCES) 149 public void interceptPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails thePreResourceShowDetails) { 150 if (isRequestAuthorized(theRequestDetails)) { 151 return; 152 } 153 IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails); 154 155 for (int i = 0; i < thePreResourceShowDetails.size(); i++) { 156 IBaseResource nextResource = thePreResourceShowDetails.getResource(i); 157 if (alreadySeenResources.putIfAbsent(nextResource, Boolean.TRUE) != null) { 158 continue; 159 } 160 161 ConsentOutcome nextOutcome = myConsentService.willSeeResource(theRequestDetails, nextResource, myContextConsentServices); 162 switch (nextOutcome.getStatus()) { 163 case PROCEED: 164 if (nextOutcome.getResource() != null) { 165 thePreResourceShowDetails.setResource(i, nextOutcome.getResource()); 166 } 167 break; 168 case AUTHORIZED: 169 break; 170 case REJECT: 171 if (nextOutcome.getResource() != null) { 172 IBaseResource newResource = nextOutcome.getResource(); 173 thePreResourceShowDetails.setResource(i, newResource); 174 alreadySeenResources.put(newResource, true); 175 } else if (nextOutcome.getOperationOutcome() != null) { 176 IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome(); 177 thePreResourceShowDetails.setResource(i, newOperationOutcome); 178 alreadySeenResources.put(newOperationOutcome, true); 179 } else { 180 String resourceId = nextResource.getIdElement().getValue(); 181 thePreResourceShowDetails.setResource(i, null); 182 nextResource.setId(resourceId); 183 } 184 break; 185 } 186 } 187 } 188 189 private IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails) { 190 return getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey); 191 } 192 193 @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE) 194 public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResource) { 195 if (theResource.getResponseResource() == null) { 196 return; 197 } 198 if (isRequestAuthorized(theRequestDetails)) { 199 return; 200 } 201 202 IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails); 203 204 // See outer resource 205 if (alreadySeenResources.putIfAbsent(theResource.getResponseResource(), Boolean.TRUE) == null) { 206 final ConsentOutcome outcome = myConsentService.willSeeResource(theRequestDetails, theResource.getResponseResource(), myContextConsentServices); 207 if (outcome.getResource() != null) { 208 theResource.setResponseResource(outcome.getResource()); 209 } 210 211 switch (outcome.getStatus()) { 212 case REJECT: 213 if (outcome.getOperationOutcome() != null) { 214 theResource.setResponseResource(outcome.getOperationOutcome()); 215 } else { 216 theResource.setResponseResource(null); 217 theResource.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT); 218 } 219 return; 220 case AUTHORIZED: 221 // Don't check children 222 return; 223 case PROCEED: 224 // Check children 225 break; 226 } 227 } 228 229 // See child resources 230 IBaseResource outerResource = theResource.getResponseResource(); 231 FhirContext ctx = theRequestDetails.getServer().getFhirContext(); 232 IModelVisitor2 visitor = new IModelVisitor2() { 233 @Override 234 public boolean acceptElement(IBase theElement, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 235 236 // Clear the total 237 if (theElement instanceof IBaseBundle) { 238 BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theElement, null); 239 } 240 241 if (theElement == outerResource) { 242 return true; 243 } 244 if (theElement instanceof IBaseResource) { 245 if (alreadySeenResources.putIfAbsent((IBaseResource) theElement, Boolean.TRUE) != null) { 246 return true; 247 } 248 ConsentOutcome childOutcome = myConsentService.willSeeResource(theRequestDetails, (IBaseResource) theElement, myContextConsentServices); 249 250 IBaseResource replacementResource = null; 251 boolean shouldReplaceResource = false; 252 boolean shouldCheckChildren = false; 253 254 switch (childOutcome.getStatus()) { 255 case REJECT: 256 replacementResource = childOutcome.getOperationOutcome(); 257 shouldReplaceResource = true; 258 break; 259 case PROCEED: 260 case AUTHORIZED: 261 replacementResource = childOutcome.getResource(); 262 shouldReplaceResource = replacementResource != null; 263 shouldCheckChildren = childOutcome.getStatus() == ConsentOperationStatusEnum.PROCEED; 264 break; 265 } 266 267 if (shouldReplaceResource) { 268 IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2); 269 BaseRuntimeChildDefinition containerChildElement = theChildDefinitionPath.get(theChildDefinitionPath.size() - 1); 270 containerChildElement.getMutator().setValue(container, replacementResource); 271 } 272 273 return shouldCheckChildren; 274 } 275 276 return true; 277 } 278 279 @Override 280 public boolean acceptUndeclaredExtension(IBaseExtension<?, ?> theNextExt, List<IBase> theContainingElementPath, List<BaseRuntimeChildDefinition> theChildDefinitionPath, List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) { 281 return true; 282 } 283 }; 284 ctx.newTerser().visit(outerResource, visitor); 285 286 } 287 288 @Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION) 289 public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) { 290 theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE); 291 myConsentService.completeOperationFailure(theRequest, theException, myContextConsentServices); 292 } 293 294 @Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY) 295 public void requestSucceeded(RequestDetails theRequest) { 296 if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) { 297 return; 298 } 299 myConsentService.completeOperationSuccess(theRequest, myContextConsentServices); 300 } 301 302 private boolean isRequestAuthorized(RequestDetails theRequestDetails) { 303 boolean retVal = false; 304 if (theRequestDetails != null) { 305 Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey); 306 retVal = Boolean.TRUE.equals(authorizedObj); 307 } 308 return retVal; 309 } 310 311 @SuppressWarnings("unchecked") 312 public static IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails, String theKey) { 313 IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>) theRequestDetails.getUserData().get(theKey); 314 if (alreadySeenResources == null) { 315 alreadySeenResources = new IdentityHashMap<>(); 316 theRequestDetails.getUserData().put(theKey, alreadySeenResources); 317 } 318 return alreadySeenResources; 319 } 320 321 private static ForbiddenOperationException toForbiddenOperationException(ConsentOutcome theOutcome) { 322 IBaseOperationOutcome operationOutcome = null; 323 if (theOutcome.getOperationOutcome() != null) { 324 operationOutcome = theOutcome.getOperationOutcome(); 325 } 326 return new ForbiddenOperationException("Rejected by consent service", operationOutcome); 327 } 328}