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