Searching for Bundles with read all Bundles permission returns 403 (#5644)
* failing test * another failing test case * fix * changelog * fix bug * spotless * cr
This commit is contained in:
parent
ec1b8fe7ee
commit
0537ab58db
|
@ -91,7 +91,7 @@ public enum BundleTypeEnum {
|
||||||
/**
|
/**
|
||||||
* Returns the enumerated value associated with this code
|
* Returns the enumerated value associated with this code
|
||||||
*/
|
*/
|
||||||
public BundleTypeEnum forCode(String theCode) {
|
public static BundleTypeEnum forCode(String theCode) {
|
||||||
BundleTypeEnum retVal = CODE_TO_ENUM.get(theCode);
|
BundleTypeEnum retVal = CODE_TO_ENUM.get(theCode);
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.model.primitive.IdDt;
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
|
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
|
@ -235,6 +236,14 @@ public class BundleUtil {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BundleTypeEnum getBundleTypeEnum(FhirContext theContext, IBaseBundle theBundle) {
|
||||||
|
String bundleTypeCode = BundleUtil.getBundleType(theContext, theBundle);
|
||||||
|
if (isBlank(bundleTypeCode)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return BundleTypeEnum.forCode(bundleTypeCode);
|
||||||
|
}
|
||||||
|
|
||||||
public static void setBundleType(FhirContext theContext, IBaseBundle theBundle, String theType) {
|
public static void setBundleType(FhirContext theContext, IBaseBundle theBundle, String theType) {
|
||||||
RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
|
RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
|
||||||
BaseRuntimeChildDefinition entryChild = def.getChildByName("type");
|
BaseRuntimeChildDefinition entryChild = def.getChildByName("type");
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 5644
|
||||||
|
title: "Previously, searching for `Bundle` resources with read all `Bundle` resources permissions, returned an
|
||||||
|
HTTP 403 Forbidden error. This was because the `AuthorizationInterceptor` applied permissions to the resources inside
|
||||||
|
the `Bundle`, instead of the `Bundle` itself. This has been fixed and permissions are no longer applied to the resources
|
||||||
|
inside a `Bundle` of type `document`, `message`, or `collection` for `Bundle` requests."
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ca.uhn.fhir.jpa.provider.r4;
|
package ca.uhn.fhir.jpa.provider.r4;
|
||||||
|
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider;
|
|
||||||
import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc;
|
import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc;
|
||||||
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
||||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||||
|
@ -14,6 +13,7 @@ import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
|
import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||||
|
@ -37,11 +37,13 @@ import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||||
import org.hl7.fhir.r4.model.Coding;
|
import org.hl7.fhir.r4.model.Coding;
|
||||||
|
import org.hl7.fhir.r4.model.Composition;
|
||||||
import org.hl7.fhir.r4.model.Condition;
|
import org.hl7.fhir.r4.model.Condition;
|
||||||
import org.hl7.fhir.r4.model.Encounter;
|
import org.hl7.fhir.r4.model.Encounter;
|
||||||
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
|
import org.hl7.fhir.r4.model.ExplanationOfBenefit;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
|
import org.hl7.fhir.r4.model.MessageHeader;
|
||||||
import org.hl7.fhir.r4.model.Observation;
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
|
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
|
||||||
import org.hl7.fhir.r4.model.Organization;
|
import org.hl7.fhir.r4.model.Organization;
|
||||||
|
@ -49,11 +51,14 @@ import org.hl7.fhir.r4.model.Parameters;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.hl7.fhir.r4.model.Practitioner;
|
import org.hl7.fhir.r4.model.Practitioner;
|
||||||
import org.hl7.fhir.r4.model.Reference;
|
import org.hl7.fhir.r4.model.Reference;
|
||||||
|
import org.hl7.fhir.r4.model.Resource;
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.hl7.fhir.r4.model.ValueSet;
|
import org.hl7.fhir.r4.model.ValueSet;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -68,7 +73,9 @@ import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.startsWith;
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Test {
|
public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Test {
|
||||||
|
@ -79,6 +86,8 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
|
||||||
private SearchParamMatcher mySearchParamMatcher;
|
private SearchParamMatcher mySearchParamMatcher;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ThreadSafeResourceDeleterSvc myThreadSafeResourceDeleterSvc;
|
private ThreadSafeResourceDeleterSvc myThreadSafeResourceDeleterSvc;
|
||||||
|
private AuthorizationInterceptor myReadAllBundleInterceptor;
|
||||||
|
private AuthorizationInterceptor myReadAllPatientInterceptor;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@Override
|
@Override
|
||||||
|
@ -87,7 +96,8 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
|
||||||
myStorageSettings.setAllowMultipleDelete(true);
|
myStorageSettings.setAllowMultipleDelete(true);
|
||||||
myStorageSettings.setExpungeEnabled(true);
|
myStorageSettings.setExpungeEnabled(true);
|
||||||
myStorageSettings.setDeleteExpungeEnabled(true);
|
myStorageSettings.setDeleteExpungeEnabled(true);
|
||||||
myServer.getRestfulServer().registerInterceptor(new BulkDataExportProvider());
|
myReadAllBundleInterceptor = new ReadAllAuthorizationInterceptor("Bundle");
|
||||||
|
myReadAllPatientInterceptor = new ReadAllAuthorizationInterceptor("Patient");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1506,4 +1516,250 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchBundles_withPermissionToSearchAllBundles_doesNotReturn403ForbiddenForDocumentBundles(){
|
||||||
|
myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor);
|
||||||
|
|
||||||
|
Bundle bundle1 = createDocumentBundle(createPatient("John", "Smith"));
|
||||||
|
Bundle bundle2 = createDocumentBundle(createPatient("Jane", "Doe"));
|
||||||
|
assertSearchContainsResources("/Bundle", bundle1, bundle2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchBundles_withPermissionToSearchAllBundles_doesNotReturn403ForbiddenForCollectionBundles(){
|
||||||
|
myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor);
|
||||||
|
|
||||||
|
Bundle bundle1 = createCollectionBundle(createPatient("John", "Smith"));
|
||||||
|
Bundle bundle2 = createCollectionBundle(createPatient("Jane", "Doe"));
|
||||||
|
assertSearchContainsResources("/Bundle", bundle1, bundle2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchBundles_withPermissionToSearchAllBundles_doesNotReturn403ForbiddenForMessageBundles(){
|
||||||
|
myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor);
|
||||||
|
|
||||||
|
Bundle bundle1 = createMessageHeaderBundle(createPatient("John", "Smith"));
|
||||||
|
Bundle bundle2 = createMessageHeaderBundle(createPatient("Jane", "Doe"));
|
||||||
|
assertSearchContainsResources("/Bundle", bundle1, bundle2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchBundles_withPermissionToViewOneBundle_onlyAllowsViewingOneBundle(){
|
||||||
|
Bundle bundle1 = createMessageHeaderBundle(createPatient("John", "Smith"));
|
||||||
|
Bundle bundle2 = createMessageHeaderBundle(createPatient("Jane", "Doe"));
|
||||||
|
|
||||||
|
myServer.getRestfulServer().getInterceptorService().registerInterceptor(
|
||||||
|
new ReadInCompartmentAuthorizationInterceptor("Bundle", bundle1.getIdElement())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertSearchContainsResources("/Bundle?_id=" + bundle1.getIdPart(), bundle1);
|
||||||
|
assertSearchFailsWith403Forbidden("/Bundle?_id=" + bundle2.getIdPart());
|
||||||
|
assertSearchFailsWith403Forbidden("/Bundle");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchPatients_withPermissionToSearchAllBundles_returns403Forbidden(){
|
||||||
|
myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor);
|
||||||
|
|
||||||
|
createPatient("John", "Smith");
|
||||||
|
createPatient("Jane", "Doe");
|
||||||
|
assertSearchFailsWith403Forbidden("/Patient");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchPatients_withPermissionToSearchAllPatients_returnsAllPatients(){
|
||||||
|
myServer.getRestfulServer().registerInterceptor(myReadAllPatientInterceptor);
|
||||||
|
|
||||||
|
Patient patient1 = createPatient("John", "Smith");
|
||||||
|
Patient patient2 = createPatient("Jane", "Doe");
|
||||||
|
assertSearchContainsResources("/Patient", patient1, patient2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchPatients_withPermissionToViewOnePatient_onlyAllowsViewingOnePatient(){
|
||||||
|
Patient patient1 = createPatient("John", "Smith");
|
||||||
|
Patient patient2 = createPatient("Jane", "Doe");
|
||||||
|
|
||||||
|
myServer.getRestfulServer().getInterceptorService().registerInterceptor(
|
||||||
|
new ReadInCompartmentAuthorizationInterceptor("Patient", patient1.getIdElement())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertSearchContainsResources("/Patient?_id=" + patient1.getIdPart(), patient1);
|
||||||
|
assertSearchFailsWith403Forbidden("/Patient?_id=" + patient2.getIdPart());
|
||||||
|
assertSearchFailsWith403Forbidden("/Patient");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingDocumentBundles_onlyRecursesOneLevelDeep() {
|
||||||
|
Bundle bundle1 = createDocumentBundle(createPatient("John", "Smith"));
|
||||||
|
Bundle bundle2 = createDocumentBundle(createPatient("John", "Smith"));
|
||||||
|
Bundle searchSet = createSearchSet(bundle1, bundle2);
|
||||||
|
|
||||||
|
RequestDetails requestDetails = new SystemRequestDetails();
|
||||||
|
requestDetails.setResourceName("Bundle");
|
||||||
|
|
||||||
|
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(requestDetails, searchSet, myFhirContext);
|
||||||
|
assertEquals(2, resources.size());
|
||||||
|
assertTrue(resources.contains(bundle1));
|
||||||
|
assertTrue(resources.contains(bundle2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingPatients_returnsPatients() {
|
||||||
|
Patient patient1 = createPatient("John", "Smith");
|
||||||
|
Patient patient2 = createPatient("Jane", "Doe");
|
||||||
|
Bundle searchSet = createSearchSet(patient1, patient2);
|
||||||
|
|
||||||
|
RequestDetails requestDetails = new SystemRequestDetails();
|
||||||
|
requestDetails.setResourceName("Patient");
|
||||||
|
|
||||||
|
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(requestDetails, searchSet, myFhirContext);
|
||||||
|
assertEquals(2, resources.size());
|
||||||
|
assertTrue(resources.contains(patient1));
|
||||||
|
assertTrue(resources.contains(patient2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(value = Bundle.BundleType.class, names = {"DOCUMENT", "COLLECTION", "MESSAGE"})
|
||||||
|
public void testShouldExamineBundleResources_withBundleRequestAndStandAloneBundleType_returnsFalse(Bundle.BundleType theBundleType){
|
||||||
|
RequestDetails requestDetails = new SystemRequestDetails();
|
||||||
|
requestDetails.setResourceName("Bundle");
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(theBundleType);
|
||||||
|
|
||||||
|
assertFalse(AuthorizationInterceptor.shouldExamineBundleChildResources(requestDetails, myFhirContext, bundle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(value = Bundle.BundleType.class, names = {"DOCUMENT", "COLLECTION", "MESSAGE"}, mode= EnumSource.Mode.EXCLUDE)
|
||||||
|
public void testShouldExamineBundleResources_withBundleRequestAndNonStandAloneBundleType_returnsTrue(Bundle.BundleType theBundleType){
|
||||||
|
RequestDetails requestDetails = new SystemRequestDetails();
|
||||||
|
requestDetails.setResourceName("Bundle");
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(theBundleType);
|
||||||
|
|
||||||
|
assertTrue(AuthorizationInterceptor.shouldExamineBundleChildResources(requestDetails, myFhirContext, bundle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(value = Bundle.BundleType.class)
|
||||||
|
public void testShouldExamineBundleResources_withNonBundleRequests_returnsTrue(Bundle.BundleType theBundleType){
|
||||||
|
RequestDetails requestDetails = new SystemRequestDetails();
|
||||||
|
requestDetails.setResourceName("Patient");
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(theBundleType);
|
||||||
|
|
||||||
|
assertTrue(AuthorizationInterceptor.shouldExamineBundleChildResources(requestDetails, myFhirContext, bundle));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Patient createPatient(String theFirstName, String theLastName){
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.addName().addGiven(theFirstName).setFamily(theLastName);
|
||||||
|
return (Patient) myPatientDao.create(patient, mySrd).getResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle createDocumentBundle(Patient thePatient){
|
||||||
|
Composition composition = new Composition();
|
||||||
|
composition.setType(new CodeableConcept().addCoding(new Coding().setSystem("http://example.org").setCode("some-type")));
|
||||||
|
composition.getSubject().setReference(thePatient.getIdElement().getValue());
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.DOCUMENT);
|
||||||
|
bundle.addEntry().setResource(composition);
|
||||||
|
bundle.addEntry().setResource(thePatient);
|
||||||
|
return (Bundle) myBundleDao.create(bundle, mySrd).getResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle createCollectionBundle(Patient thePatient) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.COLLECTION);
|
||||||
|
bundle.addEntry().setResource(thePatient);
|
||||||
|
return (Bundle) myBundleDao.create(bundle, mySrd).getResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle createMessageHeaderBundle(Patient thePatient) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.MESSAGE);
|
||||||
|
|
||||||
|
MessageHeader messageHeader = new MessageHeader();
|
||||||
|
Coding event = new Coding().setSystem("http://acme.com").setCode("some-event");
|
||||||
|
messageHeader.setEvent(event);
|
||||||
|
messageHeader.getFocusFirstRep().setReference(thePatient.getIdElement().getValue());
|
||||||
|
bundle.addEntry().setResource(messageHeader);
|
||||||
|
bundle.addEntry().setResource(thePatient);
|
||||||
|
|
||||||
|
return (Bundle) myBundleDao.create(bundle, mySrd).getResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSearchContainsResources(String theUrl, Resource... theExpectedResources){
|
||||||
|
List<String> expectedIds = Arrays.stream(theExpectedResources)
|
||||||
|
.map(resource -> resource.getIdPart())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Bundle searchResult = myClient
|
||||||
|
.search()
|
||||||
|
.byUrl(theUrl)
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
List<String> actualIds = searchResult.getEntry().stream()
|
||||||
|
.map(entry -> entry.getResource().getIdPart())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
assertEquals(expectedIds.size(), actualIds.size());
|
||||||
|
assertTrue(expectedIds.containsAll(actualIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSearchFailsWith403Forbidden(String theUrl){
|
||||||
|
try {
|
||||||
|
myClient.search().byUrl(theUrl).execute();
|
||||||
|
fail();
|
||||||
|
} catch (Exception e){
|
||||||
|
assertTrue(e.getMessage().contains("HTTP 403 Forbidden"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle createSearchSet(Resource... theResources){
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.SEARCHSET);
|
||||||
|
Arrays.stream(theResources).forEach(resource -> bundle.addEntry().setResource(resource));
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ReadAllAuthorizationInterceptor extends AuthorizationInterceptor {
|
||||||
|
|
||||||
|
private final String myResourceType;
|
||||||
|
|
||||||
|
public ReadAllAuthorizationInterceptor(String theResourceType){
|
||||||
|
super(PolicyEnum.DENY);
|
||||||
|
myResourceType = theResourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||||
|
return new RuleBuilder()
|
||||||
|
.allow().read().resourcesOfType(myResourceType).withAnyId().andThen()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ReadInCompartmentAuthorizationInterceptor extends AuthorizationInterceptor {
|
||||||
|
|
||||||
|
private final String myResourceType;
|
||||||
|
private final IIdType myId;
|
||||||
|
|
||||||
|
public ReadInCompartmentAuthorizationInterceptor(String theResourceType, IIdType theId){
|
||||||
|
super(PolicyEnum.DENY);
|
||||||
|
myResourceType = theResourceType;
|
||||||
|
myId = theId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||||
|
return new RuleBuilder()
|
||||||
|
.allow().read().allResources().inCompartment(myResourceType, myId).andThen()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,14 @@ import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.interceptor.api.Hook;
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters;
|
import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import jakarta.annotation.Nonnull;
|
import jakarta.annotation.Nonnull;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
|
@ -78,8 +80,12 @@ public class AuthorizationInterceptor implements IRuleApplier {
|
||||||
|
|
||||||
public static final String REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS =
|
public static final String REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS =
|
||||||
AuthorizationInterceptor.class.getName() + "_BulkDataExportOptions";
|
AuthorizationInterceptor.class.getName() + "_BulkDataExportOptions";
|
||||||
|
public static final String BUNDLE = "Bundle";
|
||||||
private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
|
private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class);
|
||||||
|
private static final Set<BundleTypeEnum> STANDALONE_BUNDLE_RESOURCE_TYPES =
|
||||||
|
Set.of(BundleTypeEnum.DOCUMENT, BundleTypeEnum.COLLECTION, BundleTypeEnum.MESSAGE);
|
||||||
|
|
||||||
private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
|
private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
|
||||||
private final String myRequestSeenResourcesKey =
|
private final String myRequestSeenResourcesKey =
|
||||||
AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
|
AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
|
||||||
|
@ -525,7 +531,7 @@ public class AuthorizationInterceptor implements IRuleApplier {
|
||||||
case EXTENDED_OPERATION_TYPE:
|
case EXTENDED_OPERATION_TYPE:
|
||||||
case EXTENDED_OPERATION_INSTANCE: {
|
case EXTENDED_OPERATION_INSTANCE: {
|
||||||
if (theResponseObject != null) {
|
if (theResponseObject != null) {
|
||||||
resources = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext);
|
resources = toListOfResourcesAndExcludeContainer(theRequestDetails, theResponseObject, fhirContext);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -572,22 +578,23 @@ public class AuthorizationInterceptor implements IRuleApplier {
|
||||||
OUT,
|
OUT,
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<IBaseResource> toListOfResourcesAndExcludeContainer(
|
public static List<IBaseResource> toListOfResourcesAndExcludeContainer(
|
||||||
IBaseResource theResponseObject, FhirContext fhirContext) {
|
RequestDetails theRequestDetails, IBaseResource theResponseObject, FhirContext fhirContext) {
|
||||||
if (theResponseObject == null) {
|
if (theResponseObject == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<IBaseResource> retVal;
|
List<IBaseResource> retVal;
|
||||||
|
|
||||||
boolean isContainer = false;
|
boolean shouldExamineChildResources = false;
|
||||||
if (theResponseObject instanceof IBaseBundle) {
|
if (theResponseObject instanceof IBaseBundle) {
|
||||||
isContainer = true;
|
IBaseBundle bundle = (IBaseBundle) theResponseObject;
|
||||||
|
shouldExamineChildResources = shouldExamineBundleChildResources(theRequestDetails, fhirContext, bundle);
|
||||||
} else if (theResponseObject instanceof IBaseParameters) {
|
} else if (theResponseObject instanceof IBaseParameters) {
|
||||||
isContainer = true;
|
shouldExamineChildResources = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isContainer) {
|
if (!shouldExamineChildResources) {
|
||||||
return Collections.singletonList(theResponseObject);
|
return Collections.singletonList(theResponseObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -604,6 +611,26 @@ public class AuthorizationInterceptor implements IRuleApplier {
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method determines if the given Bundle should have permissions applied to the resources inside or
|
||||||
|
* to the Bundle itself.
|
||||||
|
*
|
||||||
|
* This distinction is important in Bundle requests where a user has permissions to view all Bundles. In
|
||||||
|
* this scenario we want to apply permissions to the Bundle itself and not the resources inside if
|
||||||
|
* the Bundle is of type document, collection, or message.
|
||||||
|
*/
|
||||||
|
public static boolean shouldExamineBundleChildResources(
|
||||||
|
RequestDetails theRequestDetails, FhirContext theFhirContext, IBaseBundle theBundle) {
|
||||||
|
boolean isBundleRequest = theRequestDetails != null && BUNDLE.equals(theRequestDetails.getResourceName());
|
||||||
|
if (!isBundleRequest) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, theBundle);
|
||||||
|
boolean isStandaloneBundleResource =
|
||||||
|
bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType);
|
||||||
|
return !isStandaloneBundleResource;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Verdict {
|
public static class Verdict {
|
||||||
|
|
||||||
private final IAuthRule myDecidingRule;
|
private final IAuthRule myDecidingRule;
|
||||||
|
|
|
@ -817,7 +817,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
||||||
} else if (theOutputResource != null) {
|
} else if (theOutputResource != null) {
|
||||||
|
|
||||||
List<IBaseResource> outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(
|
List<IBaseResource> outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(
|
||||||
theOutputResource, theRequestDetails.getFhirContext());
|
theRequestDetails, theOutputResource, theRequestDetails.getFhirContext());
|
||||||
|
|
||||||
Verdict verdict = null;
|
Verdict verdict = null;
|
||||||
for (IBaseResource nextResource : outputResources) {
|
for (IBaseResource nextResource : outputResources) {
|
||||||
|
|
|
@ -3,10 +3,12 @@ package ca.uhn.fhir.util.bundle;
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
|
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
|
||||||
|
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
import ca.uhn.fhir.util.BundleBuilder;
|
import ca.uhn.fhir.util.BundleBuilder;
|
||||||
import ca.uhn.fhir.util.BundleUtil;
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
|
import jakarta.annotation.Nonnull;
|
||||||
import org.hl7.fhir.instance.model.api.IBase;
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
@ -25,8 +27,9 @@ import org.hl7.fhir.r4.model.UriType;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
|
||||||
import jakarta.annotation.Nonnull;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
@ -555,6 +558,37 @@ public class BundleUtilTest {
|
||||||
assertNull(actual);
|
assertNull(actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({
|
||||||
|
// Actual BundleType Expected BundleTypeEnum
|
||||||
|
"TRANSACTION, TRANSACTION",
|
||||||
|
"DOCUMENT, DOCUMENT",
|
||||||
|
"MESSAGE, MESSAGE",
|
||||||
|
"BATCHRESPONSE, BATCH_RESPONSE",
|
||||||
|
"TRANSACTIONRESPONSE, TRANSACTION_RESPONSE",
|
||||||
|
"HISTORY, HISTORY",
|
||||||
|
"SEARCHSET, SEARCHSET",
|
||||||
|
"COLLECTION, COLLECTION"
|
||||||
|
})
|
||||||
|
public void testGetBundleTypeEnum_withKnownBundleTypes_returnsCorrectBundleTypeEnum(Bundle.BundleType theBundleType, BundleTypeEnum theExpectedBundleTypeEnum){
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(theBundleType);
|
||||||
|
assertEquals(theExpectedBundleTypeEnum, BundleUtil.getBundleTypeEnum(ourCtx, bundle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetBundleTypeEnum_withNullBundleType_returnsNull(){
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.setType(Bundle.BundleType.NULL);
|
||||||
|
assertNull(BundleUtil.getBundleTypeEnum(ourCtx, bundle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetBundleTypeEnum_withNoBundleType_returnsNull(){
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
assertNull(BundleUtil.getBundleTypeEnum(ourCtx, bundle));
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private static Bundle withBundle(Resource theResource) {
|
private static Bundle withBundle(Resource theResource) {
|
||||||
final Bundle bundle = new Bundle();
|
final Bundle bundle = new Bundle();
|
||||||
|
|
Loading…
Reference in New Issue