make consent service dont call willSeeResource on children if parent resource is AUTHORIZED or REJECT (#6127)
This commit is contained in:
parent
55734e6cef
commit
9860511d44
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 6124
|
||||
title: "Previously, when retrieving a resource which may contain other resources, such as a document Bundle,
|
||||
if a ConsentService's willSeeResource returned AUTHORIZED or REJECT on this parent resource, the willSeeResource was
|
||||
still being called for the child resources. This has now been fixed so that if a consent service
|
||||
returns AUTHORIZED or REJECT for a parent resource, willSeeResource is not called for the child resources."
|
|
@ -1,7 +1,5 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
|
@ -11,6 +9,7 @@ import ca.uhn.fhir.jpa.entity.Search;
|
|||
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
|
||||
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||
import ca.uhn.fhir.rest.api.PreferReturnEnum;
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
|
@ -46,6 +45,7 @@ import org.apache.http.entity.StringEntity;
|
|||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Composition;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.HumanName;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
|
@ -71,11 +71,12 @@ import java.util.stream.Collectors;
|
|||
|
||||
import static org.apache.commons.lang3.StringUtils.leftPad;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -630,6 +631,73 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenWillSeeReturnsRejectForABundle_ReadingBundleThrowsResourceNotFound() {
|
||||
|
||||
myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeRejectsBundlesAuthorizesOthers());
|
||||
myServer.getRestfulServer().
|
||||
getInterceptorService().registerInterceptor(myConsentInterceptor);
|
||||
|
||||
// create bundle
|
||||
Bundle bundle = createDocumentBundle();
|
||||
MethodOutcome createOutcome = myClient.create().resource(bundle).execute();
|
||||
IIdType bundleId = createOutcome.getResource().getIdElement();
|
||||
|
||||
// read the created bundle back
|
||||
ResourceNotFoundException ex = assertThrows(ResourceNotFoundException.class,
|
||||
() -> myClient.read().resource(Bundle.class).withId(bundleId).execute());
|
||||
|
||||
assertEquals(404, ex.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenWillSeeReturnsAuthorizedForABundle_ChildResourcesInTheBundleAreVisible() {
|
||||
|
||||
myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeAuthorizesBundlesRejectsOthers());
|
||||
myServer.getRestfulServer().
|
||||
getInterceptorService().registerInterceptor(myConsentInterceptor);
|
||||
|
||||
// create bundle
|
||||
Bundle bundle = createDocumentBundle();
|
||||
MethodOutcome createOutcome = myClient.create().resource(bundle).execute();
|
||||
IIdType bundleId = createOutcome.getResource().getIdElement();
|
||||
|
||||
// read the created bundle back
|
||||
Bundle bundleRead = myClient.read().resource(Bundle.class).withId(bundleId).execute();
|
||||
|
||||
// since the consent service AUTHORIZED the bundle, the child resources in the bundle should be visible
|
||||
// because willSeeResource won't be called for the child resources once the bundle is AUTHORIZED
|
||||
assertEquals(2, bundleRead.getEntry().size());
|
||||
Composition compositionEntry = (Composition) bundleRead.getEntry().get(0).getResource();
|
||||
assertEquals("Composition/composition-in-bundle", compositionEntry.getId());
|
||||
Patient patientEntry = (Patient) bundleRead.getEntry().get(1).getResource();
|
||||
assertEquals("Patient/patient-in-bundle", patientEntry.getId());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenWillSeeReturnsProceedForABundle_WillSeeIsCalledForChildResourcesInTheBundle() {
|
||||
|
||||
myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeProceedsBundlesRejectsOthers());
|
||||
myServer.getRestfulServer().
|
||||
getInterceptorService().registerInterceptor(myConsentInterceptor);
|
||||
|
||||
|
||||
// create a bundle
|
||||
Bundle bundle = createDocumentBundle();
|
||||
MethodOutcome createOutcome = myClient.create().resource(bundle).execute();
|
||||
IIdType bundleId = createOutcome.getResource().getIdElement();
|
||||
|
||||
//read the created bundle back
|
||||
Bundle bundleRead = myClient.read().resource(Bundle.class).withId(bundleId).execute();
|
||||
|
||||
|
||||
// since the consent service replies with PROCEED for the bundle in this test case,
|
||||
// willSeeResource should be called for the child resources in the bundle and would be rejected by the
|
||||
// consent service, so the child resources in the bundle should not be visible
|
||||
assertEquals(0, bundleRead.getEntry().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the default methods all work and allow the response to proceed
|
||||
*/
|
||||
|
@ -736,7 +804,7 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
|
|||
// given
|
||||
create50Observations();
|
||||
|
||||
myConsentInterceptor = new ConsentInterceptor(new ConsentSvcRejectWillSeeResource());
|
||||
myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeProceedsBundlesRejectsOthers());
|
||||
myServer.getRestfulServer().getInterceptorService().registerInterceptor(myConsentInterceptor);
|
||||
|
||||
// when
|
||||
|
@ -754,7 +822,7 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
|
|||
// given
|
||||
create50Observations();
|
||||
|
||||
myConsentInterceptor = new ConsentInterceptor(new ConsentSvcRejectWillSeeResource());
|
||||
myConsentInterceptor = new ConsentInterceptor(new ConsentSvcWillSeeProceedsBundlesRejectsOthers());
|
||||
myServer.getRestfulServer().getInterceptorService().registerInterceptor(myConsentInterceptor);
|
||||
|
||||
// when
|
||||
|
@ -767,6 +835,21 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
|
|||
|
||||
}
|
||||
|
||||
private Bundle createDocumentBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.setType(Bundle.BundleType.DOCUMENT);
|
||||
|
||||
Composition composition = new Composition();
|
||||
composition.setId("composition-in-bundle");
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.setId("patient-in-bundle");
|
||||
|
||||
bundle.addEntry().setResource(composition);
|
||||
bundle.addEntry().setResource(patient);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private void createPatientAndOrg() {
|
||||
myPatientIds = new ArrayList<>();
|
||||
|
||||
|
@ -1095,7 +1178,7 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
|
|||
|
||||
}
|
||||
|
||||
private static class ConsentSvcRejectWillSeeResource implements IConsentService {
|
||||
private static class ConsentSvcWillSeeProceedsBundlesRejectsOthers implements IConsentService {
|
||||
@Override
|
||||
public ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
|
||||
if("Bundle".equals(theResource.fhirType())){
|
||||
|
@ -1105,5 +1188,27 @@ public class ConsentInterceptorResourceProviderR4IT extends BaseResourceProvider
|
|||
}
|
||||
|
||||
}
|
||||
private static class ConsentSvcWillSeeAuthorizesBundlesRejectsOthers implements IConsentService {
|
||||
@Override
|
||||
public ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
|
||||
if("Bundle".equals(theResource.fhirType())){
|
||||
return new ConsentOutcome(ConsentOperationStatusEnum.AUTHORIZED);
|
||||
}
|
||||
return new ConsentOutcome(ConsentOperationStatusEnum.REJECT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ConsentSvcWillSeeRejectsBundlesAuthorizesOthers implements IConsentService {
|
||||
@Override
|
||||
public ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
|
||||
if("Bundle".equals(theResource.fhirType())){
|
||||
return new ConsentOutcome(ConsentOperationStatusEnum.REJECT);
|
||||
}
|
||||
return new ConsentOutcome(ConsentOperationStatusEnum.AUTHORIZED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
|||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
@ -508,8 +507,7 @@ public class AuthorizationInterceptor implements IRuleApplier {
|
|||
}
|
||||
|
||||
// Don't check the value twice
|
||||
IdentityHashMap<IBaseResource, Boolean> alreadySeenMap =
|
||||
ConsentInterceptor.getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
|
||||
IdentityHashMap<IBaseResource, Boolean> alreadySeenMap = getAlreadySeenResourcesMap(theRequestDetails);
|
||||
if (alreadySeenMap.putIfAbsent(theResponseObject, Boolean.TRUE) != null) {
|
||||
return;
|
||||
}
|
||||
|
@ -678,4 +676,15 @@ public class AuthorizationInterceptor implements IRuleApplier {
|
|||
|
||||
return theResource.getIdElement().getResourceType();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails) {
|
||||
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>)
|
||||
theRequestDetails.getUserData().get(myRequestSeenResourcesKey);
|
||||
if (alreadySeenResources == null) {
|
||||
alreadySeenResources = new IdentityHashMap<>();
|
||||
theRequestDetails.getUserData().put(myRequestSeenResourcesKey, alreadySeenResources);
|
||||
}
|
||||
return alreadySeenResources;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,7 +221,8 @@ public class ConsentInterceptor {
|
|||
return;
|
||||
}
|
||||
|
||||
IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
|
||||
IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
|
||||
getAlreadySeenResourcesMap(theRequestDetails);
|
||||
for (int resourceIdx = 0; resourceIdx < thePreResourceAccessDetails.size(); resourceIdx++) {
|
||||
IBaseResource nextResource = thePreResourceAccessDetails.getResource(resourceIdx);
|
||||
for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) {
|
||||
|
@ -243,10 +244,11 @@ public class ConsentInterceptor {
|
|||
case PROCEED:
|
||||
break;
|
||||
case AUTHORIZED:
|
||||
authorizedResources.put(nextResource, Boolean.TRUE);
|
||||
alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.AUTHORIZED);
|
||||
skipSubsequentServices = true;
|
||||
break;
|
||||
case REJECT:
|
||||
alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.REJECT);
|
||||
thePreResourceAccessDetails.setDontReturnResourceAtIndex(resourceIdx);
|
||||
skipSubsequentServices = true;
|
||||
break;
|
||||
|
@ -307,12 +309,14 @@ public class ConsentInterceptor {
|
|||
return;
|
||||
}
|
||||
|
||||
IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
|
||||
IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
|
||||
getAlreadySeenResourcesMap(theRequestDetails);
|
||||
|
||||
for (int i = 0; i < thePreResourceShowDetails.size(); i++) {
|
||||
|
||||
IBaseResource resource = thePreResourceShowDetails.getResource(i);
|
||||
if (resource == null || authorizedResources.putIfAbsent(resource, Boolean.TRUE) != null) {
|
||||
if (resource == null
|
||||
|| alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -329,15 +333,17 @@ public class ConsentInterceptor {
|
|||
}
|
||||
continue;
|
||||
case AUTHORIZED:
|
||||
alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED);
|
||||
if (newResource != null) {
|
||||
thePreResourceShowDetails.setResource(i, newResource);
|
||||
}
|
||||
continue;
|
||||
case REJECT:
|
||||
alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT);
|
||||
if (nextOutcome.getOperationOutcome() != null) {
|
||||
IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome();
|
||||
thePreResourceShowDetails.setResource(i, newOperationOutcome);
|
||||
authorizedResources.put(newOperationOutcome, true);
|
||||
alreadySeenResources.put(newOperationOutcome, ConsentOperationStatusEnum.PROCEED);
|
||||
} else {
|
||||
resource = null;
|
||||
thePreResourceShowDetails.setResource(i, null);
|
||||
|
@ -349,8 +355,8 @@ public class ConsentInterceptor {
|
|||
}
|
||||
|
||||
@Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE)
|
||||
public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResource) {
|
||||
if (theResource.getResponseResource() == null) {
|
||||
public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails) {
|
||||
if (theResponseDetails.getResponseResource() == null) {
|
||||
return;
|
||||
}
|
||||
if (isRequestAuthorized(theRequestDetails)) {
|
||||
|
@ -366,35 +372,56 @@ public class ConsentInterceptor {
|
|||
return;
|
||||
}
|
||||
|
||||
IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
|
||||
// Take care of outer resource first
|
||||
IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
|
||||
getAlreadySeenResourcesMap(theRequestDetails);
|
||||
if (alreadySeenResources.containsKey(theResponseDetails.getResponseResource())) {
|
||||
// we've already seen this resource before
|
||||
ConsentOperationStatusEnum decisionOnResource =
|
||||
alreadySeenResources.get(theResponseDetails.getResponseResource());
|
||||
|
||||
// See outer resource
|
||||
if (authorizedResources.putIfAbsent(theResource.getResponseResource(), Boolean.TRUE) == null) {
|
||||
if (ConsentOperationStatusEnum.AUTHORIZED.equals(decisionOnResource)
|
||||
|| ConsentOperationStatusEnum.REJECT.equals(decisionOnResource)) {
|
||||
// the consent service decision on the resource was AUTHORIZED or REJECT.
|
||||
// In both cases, we can immediately return without checking children
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// we haven't seen this resource before
|
||||
// mark it as seen now, set the initial consent decision value to PROCEED by default,
|
||||
// we will update if it changes another value below
|
||||
alreadySeenResources.put(theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.PROCEED);
|
||||
|
||||
for (IConsentService next : myConsentService) {
|
||||
final ConsentOutcome outcome = next.willSeeResource(
|
||||
theRequestDetails, theResource.getResponseResource(), myContextConsentServices);
|
||||
theRequestDetails, theResponseDetails.getResponseResource(), myContextConsentServices);
|
||||
if (outcome.getResource() != null) {
|
||||
theResource.setResponseResource(outcome.getResource());
|
||||
theResponseDetails.setResponseResource(outcome.getResource());
|
||||
}
|
||||
|
||||
// Clear the total
|
||||
if (theResource.getResponseResource() instanceof IBaseBundle) {
|
||||
if (theResponseDetails.getResponseResource() instanceof IBaseBundle) {
|
||||
BundleUtil.setTotal(
|
||||
theRequestDetails.getFhirContext(), (IBaseBundle) theResource.getResponseResource(), null);
|
||||
theRequestDetails.getFhirContext(),
|
||||
(IBaseBundle) theResponseDetails.getResponseResource(),
|
||||
null);
|
||||
}
|
||||
|
||||
switch (outcome.getStatus()) {
|
||||
case REJECT:
|
||||
alreadySeenResources.put(
|
||||
theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.REJECT);
|
||||
if (outcome.getOperationOutcome() != null) {
|
||||
theResource.setResponseResource(outcome.getOperationOutcome());
|
||||
theResponseDetails.setResponseResource(outcome.getOperationOutcome());
|
||||
} else {
|
||||
theResource.setResponseResource(null);
|
||||
theResource.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
|
||||
theResponseDetails.setResponseResource(null);
|
||||
theResponseDetails.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
|
||||
}
|
||||
// Return immediately
|
||||
return;
|
||||
case AUTHORIZED:
|
||||
alreadySeenResources.put(
|
||||
theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.AUTHORIZED);
|
||||
// Don't check children, so return immediately
|
||||
return;
|
||||
case PROCEED:
|
||||
|
@ -405,7 +432,7 @@ public class ConsentInterceptor {
|
|||
}
|
||||
|
||||
// See child resources
|
||||
IBaseResource outerResource = theResource.getResponseResource();
|
||||
IBaseResource outerResource = theResponseDetails.getResponseResource();
|
||||
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
|
||||
IModelVisitor2 visitor = new IModelVisitor2() {
|
||||
@Override
|
||||
|
@ -425,7 +452,7 @@ public class ConsentInterceptor {
|
|||
}
|
||||
if (theElement instanceof IBaseResource) {
|
||||
IBaseResource resource = (IBaseResource) theElement;
|
||||
if (authorizedResources.putIfAbsent(resource, Boolean.TRUE) != null) {
|
||||
if (alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -441,12 +468,17 @@ public class ConsentInterceptor {
|
|||
case REJECT:
|
||||
replacementResource = childOutcome.getOperationOutcome();
|
||||
shouldReplaceResource = true;
|
||||
alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT);
|
||||
break;
|
||||
case PROCEED:
|
||||
replacementResource = childOutcome.getResource();
|
||||
shouldReplaceResource = replacementResource != null;
|
||||
break;
|
||||
case AUTHORIZED:
|
||||
replacementResource = childOutcome.getResource();
|
||||
shouldReplaceResource = replacementResource != null;
|
||||
shouldCheckChildren &= childOutcome.getStatus() == ConsentOperationStatusEnum.PROCEED;
|
||||
shouldCheckChildren = false;
|
||||
alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -477,10 +509,6 @@ public class ConsentInterceptor {
|
|||
ctx.newTerser().visit(outerResource, visitor);
|
||||
}
|
||||
|
||||
private IdentityHashMap<IBaseResource, Boolean> getAuthorizedResourcesMap(RequestDetails theRequestDetails) {
|
||||
return getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
|
||||
}
|
||||
|
||||
@Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION)
|
||||
public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) {
|
||||
theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE);
|
||||
|
@ -570,14 +598,23 @@ public class ConsentInterceptor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The map returned by this method keeps track of the resources already processed by ConsentInterceptor in the
|
||||
* context of a request.
|
||||
* If the map contains a particular resource, it means that the resource has already been processed and the value
|
||||
* is the status returned by consent services for that resource.
|
||||
* @param theRequestDetails
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(
|
||||
RequestDetails theRequestDetails, String theKey) {
|
||||
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>)
|
||||
theRequestDetails.getUserData().get(theKey);
|
||||
private IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> getAlreadySeenResourcesMap(
|
||||
RequestDetails theRequestDetails) {
|
||||
IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
|
||||
(IdentityHashMap<IBaseResource, ConsentOperationStatusEnum>)
|
||||
theRequestDetails.getUserData().get(myRequestSeenResourcesKey);
|
||||
if (alreadySeenResources == null) {
|
||||
alreadySeenResources = new IdentityHashMap<>();
|
||||
theRequestDetails.getUserData().put(theKey, alreadySeenResources);
|
||||
theRequestDetails.getUserData().put(myRequestSeenResourcesKey, alreadySeenResources);
|
||||
}
|
||||
return alreadySeenResources;
|
||||
}
|
||||
|
|
|
@ -580,7 +580,10 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
List<IBaseResource> output =
|
||||
fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails);
|
||||
if (output.size() == 1) {
|
||||
return theResource;
|
||||
// do not return theResource here but return whatever the interceptor returned in the list because
|
||||
// the interceptor might have set the resource in the list to null (if it didn't want it to be returned).
|
||||
// ConsentInterceptor might do this for example.
|
||||
return (T) output.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.apache.http.client.methods.HttpGet;
|
|||
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Composition;
|
||||
import org.hl7.fhir.r4.model.Identifier;
|
||||
import org.hl7.fhir.r4.model.OperationOutcome;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
|
@ -61,10 +62,13 @@ import java.util.List;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -79,11 +83,14 @@ public class ConsentInterceptorTest {
|
|||
private int myPort;
|
||||
private static final DummyPatientResourceProvider ourPatientProvider = new DummyPatientResourceProvider(ourCtx);
|
||||
private static final DummySystemProvider ourSystemProvider = new DummySystemProvider();
|
||||
private static final HashMapResourceProvider<Bundle> ourBundleProvider =
|
||||
new HashMapResourceProvider<>(ourCtx, Bundle.class);
|
||||
|
||||
@RegisterExtension
|
||||
static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
|
||||
.registerProvider(ourPatientProvider)
|
||||
.registerProvider(ourSystemProvider)
|
||||
.registerProvider(ourBundleProvider)
|
||||
.withPagingProvider(new FifoMemoryPagingProvider(10));
|
||||
|
||||
@Mock(answer = Answers.CALLS_REAL_METHODS)
|
||||
|
@ -109,6 +116,7 @@ public class ConsentInterceptorTest {
|
|||
|
||||
ourServer.registerInterceptor(myInterceptor);
|
||||
ourPatientProvider.clear();
|
||||
ourBundleProvider.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -494,6 +502,115 @@ public class ConsentInterceptorTest {
|
|||
verifyNoMoreInteractions(myConsentSvc);
|
||||
}
|
||||
|
||||
private Bundle createDocumentBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.setType(Bundle.BundleType.DOCUMENT);
|
||||
bundle.setId("test-bundle-id");
|
||||
Composition composition = new Composition();
|
||||
composition.setId("composition-in-bundle");
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.setId("patient-in-bundle");
|
||||
|
||||
bundle.addEntry().setResource(composition);
|
||||
bundle.addEntry().setResource(patient);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenCanSeeReturnsRejectForBundle_WillSeeIsNotCalled() throws IOException {
|
||||
ourBundleProvider.store(createDocumentBundle());
|
||||
when(myConsentSvc.canSeeResource(any(),isA(Bundle.class),any())).thenReturn(ConsentOutcome.REJECT);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Bundle/test-bundle-id");
|
||||
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
|
||||
assertEquals(404, status.getStatusLine().getStatusCode());
|
||||
// response should be an error outcome instead of the resource
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
|
||||
OperationOutcome outcome = ourCtx.newJsonParser().parseResource(OperationOutcome.class, responseContent);
|
||||
assertTrue(outcome.hasIssue());
|
||||
assertEquals(OperationOutcome.IssueSeverity.ERROR, outcome.getIssueFirstRep().getSeverity());
|
||||
}
|
||||
|
||||
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
|
||||
// willSee should not be called, even for the bundle
|
||||
verify(myConsentSvc, times(0)).willSeeResource(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenCanSeeReturnsAuthorizedForBundle_WillSeeIsNotCalled() throws IOException {
|
||||
ourBundleProvider.store(createDocumentBundle());
|
||||
when(myConsentSvc.canSeeResource(any(),isA(Bundle.class),any())).thenReturn(ConsentOutcome.AUTHORIZED);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Bundle/test-bundle-id");
|
||||
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
|
||||
// willSee should not be called, even for the bundle
|
||||
verify(myConsentSvc, times(0)).willSeeResource(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenWillSeeReturnsRejectForBundle_WillSeeIsNotCalledForChildResources() throws IOException {
|
||||
ourBundleProvider.store(createDocumentBundle());
|
||||
when(myConsentSvc.canSeeResource(any(),any(),any())).thenReturn(ConsentOutcome.PROCEED);
|
||||
when(myConsentSvc.willSeeResource(any(),isA(Bundle.class),any())).thenReturn(ConsentOutcome.REJECT);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Bundle/test-bundle-id");
|
||||
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
|
||||
assertEquals(404, status.getStatusLine().getStatusCode());
|
||||
// response should be an error outcome instead of the resource
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
|
||||
OperationOutcome outcome = ourCtx.newJsonParser().parseResource(OperationOutcome.class, responseContent);
|
||||
assertTrue(outcome.hasIssue());
|
||||
assertEquals(OperationOutcome.IssueSeverity.ERROR, outcome.getIssueFirstRep().getSeverity());
|
||||
}
|
||||
|
||||
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
|
||||
// will see should be called only once, for the bundle
|
||||
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenWillSeeReturnsAuthorizedForBundle_WillSeeIsNotCalledForChildResources() throws IOException {
|
||||
ourBundleProvider.store(createDocumentBundle());
|
||||
when(myConsentSvc.canSeeResource(any(),any(),any())).thenReturn(ConsentOutcome.PROCEED);
|
||||
when(myConsentSvc.willSeeResource(any(),isA(Bundle.class),any())).thenReturn(ConsentOutcome.AUTHORIZED);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Bundle/test-bundle-id");
|
||||
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
|
||||
// willSee should only be called once, for the bundle
|
||||
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetBundle_WhenWillSeeReturnsProceedForBundle_WillSeeIsCalledForChildResources() throws IOException {
|
||||
ourBundleProvider.store(createDocumentBundle());
|
||||
|
||||
when(myConsentSvc.canSeeResource(any(),any(),any())).thenReturn(ConsentOutcome.PROCEED);
|
||||
when(myConsentSvc.willSeeResource(any(),isA(Bundle.class),any())).thenReturn(ConsentOutcome.PROCEED);
|
||||
// the test bundle contains a Composition and a Patient, we expect calls to them in this case
|
||||
when(myConsentSvc.willSeeResource(any(),isA(Composition.class),any())).thenReturn(ConsentOutcome.PROCEED);
|
||||
when(myConsentSvc.willSeeResource(any(),isA(Patient.class),any())).thenReturn(ConsentOutcome.PROCEED);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Bundle/test-bundle-id");
|
||||
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
|
||||
// expect willSee to be called 3 times: 1 for the bundle, 1 for composition child and 1 for Patient child
|
||||
verify(myConsentSvc, times(1)).willSeeResource(any(), isA(Bundle.class), any());
|
||||
verify(myConsentSvc, times(1)).willSeeResource(any(), isA(Composition.class), any());
|
||||
verify(myConsentSvc, times(1)).willSeeResource(any(), isA(Patient.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPage_SeeResourceReplacesInnerResource() throws IOException {
|
||||
Patient pta = (Patient) new Patient().setActive(true).setId("PTA");
|
||||
|
|
Loading…
Reference in New Issue