Fix authorization handling for Bundle resources in the output (#5953)

* Fix authorization handling for Bundle resources in the output. When the Bundle is standalone, authorization should not be checked for resources included in the Bundle.

* Add some more tests to cover more use cases.

* Add more tests. Exclude collection type as it can be returned by a custom operation and it is not a standalone type.

* Address code review comment. Remove unused method.
This commit is contained in:
Martha Mitran 2024-05-28 06:00:06 -07:00 committed by GitHub
parent 357802bfe8
commit a9815c8857
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 345 additions and 231 deletions

View File

@ -0,0 +1,6 @@
---
type: add
issue: 5952
title: "Previously, since hapi-fhir 7.0, when retrieving the consecutive pages of a Bundle resource search operation
using a client with read permissions for Bundle resources, the request would fail with a 403 Forbidden error.
This has been fixed."

View File

@ -12,11 +12,8 @@ import ca.uhn.fhir.jpa.term.TermTestUtil;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
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.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
@ -64,7 +61,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
@ -73,15 +71,17 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -95,9 +95,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
private SearchParamMatcher mySearchParamMatcher;
@Autowired
private ThreadSafeResourceDeleterSvc myThreadSafeResourceDeleterSvc;
private AuthorizationInterceptor myReadAllBundleInterceptor;
private AuthorizationInterceptor myReadAllPatientInterceptor;
private AuthorizationInterceptor myWriteResourcesInTransactionAuthorizationInterceptor;
@BeforeEach
@Override
@ -106,9 +103,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
myStorageSettings.setAllowMultipleDelete(true);
myStorageSettings.setExpungeEnabled(true);
myStorageSettings.setDeleteExpungeEnabled(true);
myReadAllBundleInterceptor = new ReadAllAuthorizationInterceptor("Bundle");
myReadAllPatientInterceptor = new ReadAllAuthorizationInterceptor("Patient");
myWriteResourcesInTransactionAuthorizationInterceptor = new WriteResourcesInTransactionAuthorizationInterceptor();
}
@Override
@ -116,6 +110,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
public void after() throws Exception {
super.after();
myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof AuthorizationInterceptor);
myClient.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof SimpleRequestHeaderInterceptor);
}
/**
@ -232,9 +227,8 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
return new RuleBuilder()
.allow().create().resourcesOfType("Patient").withAnyId().withTester(new IAuthRuleTester() {
@Override
public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) {
if (theInputResource instanceof Patient) {
Patient patient = (Patient) theInputResource;
public boolean matches(RuleTestRequest theRequest) {
if (theRequest.resource instanceof Patient patient) {
return patient
.getIdentifier()
.stream()
@ -317,53 +311,121 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
public static Stream<Arguments> getReadPatientArguments() {
return Stream.of(
Arguments.of(new ReadAllOfTypeAuthorizationInterceptor("Bundle"), false),
Arguments.of(new ReadAllOfTypeAuthorizationInterceptor("Patient"), true)
);
}
@Test
public void testReadInTransaction() {
public static Stream<Arguments> getReadPatientInTransactionArguments() {
return Stream.of(
Arguments.of(new ReadAllOfTypeAndTransactionAuthorizationInterceptor("Bundle"), false),
Arguments.of(new ReadAllOfTypeAndTransactionAuthorizationInterceptor("Patient"), true)
);
}
Patient patient = new Patient();
@ParameterizedTest
@MethodSource(value = "getReadPatientArguments")
public void testReadPatientById(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
IIdType patient = createPatient();
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
assertReadByIdAllowed(patient, theShouldAllow);
}
@ParameterizedTest
@MethodSource(value = "getReadPatientInTransactionArguments")
public void testReadPatientInTransaction(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
final Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad");
IIdType id = myClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute().getId().toUnqualifiedVersionless();
myServer.getRestfulServer().registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
String authHeader = theRequestDetails.getHeader("Authorization");
if (!"Bearer AAA".equals(authHeader)) {
throw new AuthenticationException("Invalid auth header: " + authHeader);
}
return new RuleBuilder()
.allow().transaction().withAnyOperation().andApplyNormalRules().andThen()
.allow().read().resourcesOfType(Patient.class).withAnyId()
.build();
}
});
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
SimpleRequestHeaderInterceptor interceptor = new SimpleRequestHeaderInterceptor("Authorization", "Bearer AAA");
try {
myClient.registerInterceptor(interceptor);
// Read Patient by id
Bundle bundle = new Bundle().setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl(id.getValue());
assertTransactionAllowed(bundle, theShouldAllow);
// Search all Patients
bundle = new Bundle().setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Patient?");
assertTransactionAllowed(bundle, theShouldAllow);
} finally {
myClient.unregisterInterceptor(interceptor);
}
}
public static Stream<Arguments> getReadStandaloneBundleArguments() {
return Stream.of(
Arguments.of(new ReadAllOfTypeAuthorizationInterceptor("Bundle"), true),
Arguments.of(new ReadAllOfTypeAuthorizationInterceptor("Patient"), false)
);
}
public static Stream<Arguments> getReadStandaloneBundleInTransactionArguments() {
return Stream.of(
Arguments.of(new ReadAllOfTypeAndTransactionAuthorizationInterceptor("Bundle"), true),
Arguments.of(new ReadAllOfTypeAndTransactionAuthorizationInterceptor("Patient"), false)
);
}
@ParameterizedTest
@MethodSource(value = "getReadStandaloneBundleArguments")
public void testReadBundleById(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
Bundle bundle = createDocumentBundle(createPatient("John", "Smith"));
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
assertReadByIdAllowed(bundle.getIdElement(), theShouldAllow);
}
@ParameterizedTest
@MethodSource(value = "getReadStandaloneBundleInTransactionArguments")
public void testReadBundleInTransaction(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
Bundle documentBundle1 = createDocumentBundle(createPatient("John", "Smith"));
createDocumentBundle(createPatient("Jane", "Doe"));
IIdType collectionBundle1Id = documentBundle1.getIdElement();
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
SimpleRequestHeaderInterceptor interceptor = new SimpleRequestHeaderInterceptor("Authorization", "Bearer AAA");
try {
myClient.registerInterceptor(interceptor);
Bundle bundle;
Bundle responseBundle;
// Read
// Read Bundle
bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl(id.getValue());
responseBundle = myClient.transaction().withBundle(bundle).execute();
patient = (Patient) responseBundle.getEntry().get(0).getResource();
assertEquals("Tester", patient.getNameFirstRep().getFamily());
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl(collectionBundle1Id.toUnqualifiedVersionless().getValue());
assertTransactionAllowed(bundle, theShouldAllow);
// Search
bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Patient?");
responseBundle = myClient.transaction().withBundle(bundle).execute();
responseBundle = (Bundle) responseBundle.getEntry().get(0).getResource();
patient = (Patient) responseBundle.getEntry().get(0).getResource();
assertEquals("Tester", patient.getNameFirstRep().getFamily());
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Bundle?type=collection");
assertTransactionAllowed(bundle, theShouldAllow);
// Simple Search count 1
Bundle responseBundle = assertSearchAllowed("/Bundle", theShouldAllow);
// Get next page
if (responseBundle != null) {
Bundle.BundleLinkComponent next = responseBundle.getLink("next");
assertNotNull(next);
bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("/Bundle?" + Constants.PARAM_PAGINGACTION + next.getUrl());
assertTransactionAllowed(bundle, theShouldAllow);
}
} finally {
myClient.unregisterInterceptor(interceptor);
}
@ -396,7 +458,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
});
Bundle bundle;
Observation response;
// Read (no masking)
@ -412,7 +473,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
.elementsSubset("status")
.execute();
assertEquals(ObservationStatus.FINAL, response.getStatus());
assertEquals(null, response.getSubject().getReference());
assertNull(response.getSubject().getReference());
// Read a non-allowed observation
try {
@ -481,7 +542,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
patient.setId("Patient/A");
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad");
IIdType id = myClient.update().resource(patient).execute().getId();
myClient.update().resource(patient).execute();
Observation obs = new Observation();
obs.setId("Observation/B");
@ -796,12 +857,10 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
post = new HttpPost(myServerBase + "/Patient");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
response = ourHttpClient.execute(post);
final IdType id2;
try {
assertEquals(201, response.getStatusLine().getStatusCode());
String newIdString = response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue();
assertThat(newIdString, startsWith(myServerBase + "/Patient/"));
id2 = new IdType(newIdString);
} finally {
response.close();
}
@ -945,7 +1004,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
httpGet = new HttpGet(myServerBase + "/Patient/B/$graphql?query=" + UrlUtil.escapeUrlParam(query));
try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
assertEquals(403, response.getStatusLine().getStatusCode());
}
@ -1185,7 +1243,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
myClient.create().resource(enc).execute();
try {
outcome = myClient
myClient
.operation()
.onInstance(pid1)
.named(JpaConstants.OPERATION_EVERYTHING)
@ -1230,9 +1288,10 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
});
String patchBody = "[\n" +
" { \"op\": \"replace\", \"path\": \"/status\", \"value\": \"amended\" }\n" +
" ]";
String patchBody = """
[
{ "op": "replace", "path": "/status", "value": "amended" }
]""";
// Allowed
myClient.patch().withBody(patchBody).withId(oid1).execute();
@ -1418,7 +1477,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
{
String url = "/ExplanationOfBenefit?patient=" + p1id.getIdPart() + "," + p3id.getIdPart();
try {
Bundle result = myClient.search().byUrl(url).returnBundle(Bundle.class).execute();
myClient.search().byUrl(url).returnBundle(Bundle.class).execute();
fail();
} catch (ForbiddenOperationException e) {
assertThat(e.getMessage(), startsWith("HTTP 403 Forbidden: " + Msg.code(333) + "Access denied by rule"));
@ -1641,40 +1700,39 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
@Test
public void testSearchBundles_withPermissionToSearchAllBundles_doesNotReturn403ForbiddenForDocumentBundles(){
myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor);
@ParameterizedTest
@MethodSource(value = "getReadStandaloneBundleArguments")
public void testGetNextPage_forDocumentBundles(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
Bundle bundle = createDocumentBundle(createPatient("John", "Smith"));
Bundle firstBundle = new Bundle();
firstBundle.addLink().setRelation("next").setUrl(myClient.getServerBase() + "/"+ bundle.getIdElement().toUnqualifiedVersionless());
assertGetNextPageAllowed(firstBundle, theShouldAllow);
}
Bundle bundle1 = createDocumentBundle(createPatient("John", "Smith"));
Bundle bundle2 = createDocumentBundle(createPatient("Jane", "Doe"));
assertSearchContainsResources("/Bundle", bundle1, bundle2);
@ParameterizedTest
@MethodSource(value = "getReadStandaloneBundleArguments")
public void testSearchBundles_forDocumentBundles(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
createDocumentBundle(createPatient("John", "Smith"));
assertSearchAllowed("/Bundle", theShouldAllow);
}
@ParameterizedTest
@MethodSource(value = "getReadStandaloneBundleArguments")
public void testSearchBundles_forMessageBundles(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
createMessageHeaderBundle(createPatient("John", "Smith"));
assertSearchAllowed("/Bundle", theShouldAllow);
}
@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(){
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())
new ReadInCompartmentAuthorizationInterceptor("Bundle", bundle1.getIdElement())
);
assertSearchContainsResources("/Bundle?_id=" + bundle1.getIdPart(), bundle1);
@ -1682,26 +1740,16 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
assertSearchFailsWith403Forbidden("/Bundle");
}
@Test
public void testSearchPatients_withPermissionToSearchAllBundles_returns403Forbidden(){
myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor);
@ParameterizedTest
@MethodSource(value = "getReadPatientArguments")
public void testSearchPatients(AuthorizationInterceptor theAuthorizationInterceptor, boolean theShouldAllow) {
myServer.getRestfulServer().registerInterceptor(theAuthorizationInterceptor);
createPatient("John", "Smith");
createPatient("Jane", "Doe");
assertSearchFailsWith403Forbidden("/Patient");
assertSearchAllowed("/Patient", theShouldAllow);
}
@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(){
public void testSearchPatients_withPermissionToViewOnePatient_onlyAllowsViewingOnePatient() {
Patient patient1 = createPatient("John", "Smith");
Patient patient2 = createPatient("Jane", "Doe");
@ -1714,72 +1762,9 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
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));
}
@ParameterizedTest
@ValueSource(strings = {"collection", "document", "message"})
public void testPermissionsToPostTransaction_withValidNestedBundleRequest_successfullyPostsTransactions(String theBundleType){
@ValueSource(strings = {"document", "message", "collection"})
public void testTransactionBundle_withNestedNonTransactionBundle_allowed(String theBundleType) {
BundleBuilder builder = new BundleBuilder(myFhirContext);
builder.setType(theBundleType);
IBaseBundle nestedBundle = builder.getBundle();
@ -1788,7 +1773,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
builder.addTransactionCreateEntry(nestedBundle);
IBaseBundle transaction = builder.getBundle();
myServer.getRestfulServer().registerInterceptor(myWriteResourcesInTransactionAuthorizationInterceptor);
myServer.getRestfulServer().registerInterceptor(new WriteResourcesInTransactionAuthorizationInterceptor());
myClient
.transaction()
@ -1806,7 +1791,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
@ParameterizedTest
@NullSource
@ValueSource(strings = {"", "/"})
public void testPermissionsToPostTransaction_withInvalidNestedBundleRequest_blocksTransaction(String theInvalidUrl){
public void testTransactionBundle_withNestedTransactionBundle_notAllowed(String theInvalidUrl) {
// inner transaction
Patient patient = new Patient();
BundleBuilder builder = new BundleBuilder(myFhirContext);
@ -1820,7 +1805,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
entry.setResource(innerTransaction);
entry.getRequest().setUrl(theInvalidUrl).setMethod(Bundle.HTTPVerb.POST);
myServer.getRestfulServer().registerInterceptor(myWriteResourcesInTransactionAuthorizationInterceptor);
myServer.getRestfulServer().registerInterceptor(new WriteResourcesInTransactionAuthorizationInterceptor());
try {
myClient
@ -1838,9 +1823,9 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
@Test
public void testPermissionToPostTransaction_withUpdateParameters_blocksTransaction(){
public void testTransactionBundle_withUpdateParameters_blocksTransaction() {
DateType originalBirthDate = new DateType("2000-01-01");
Patient patient = createPatient(originalBirthDate);
createPatient(originalBirthDate);
DateType newBirthDate = new DateType("2005-01-01");
Parameters birthDatePatch = createPatientBirthdatePatch(newBirthDate);
@ -1849,7 +1834,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
bundleBuilder.addTransactionUpdateEntry(birthDatePatch);
IBaseBundle transaction = bundleBuilder.getBundle();
myServer.getRestfulServer().registerInterceptor(myWriteResourcesInTransactionAuthorizationInterceptor);
myServer.getRestfulServer().registerInterceptor(new WriteResourcesInTransactionAuthorizationInterceptor());
try {
myClient
@ -1870,7 +1855,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
@Test
public void testPermissionToPostTransaction_withPatchParameters_successfullyPostsTransaction(){
public void testTransactionBundle_withPatchParameters_allowed() {
DateType originalBirthDate = new DateType("2000-01-01");
Patient patient = createPatient(originalBirthDate);
@ -1881,7 +1866,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
bundleBuilder.addTransactionFhirPatchEntry(patient.getIdElement(), birthDatePatch);
IBaseBundle transaction = bundleBuilder.getBundle();
myServer.getRestfulServer().registerInterceptor(myWriteResourcesInTransactionAuthorizationInterceptor);
myServer.getRestfulServer().registerInterceptor(new WriteResourcesInTransactionAuthorizationInterceptor());
myClient
.transaction()
@ -1895,7 +1880,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
assertEquals(newBirthDate.getValueAsString(), savedPatient.getBirthDateElement().getValueAsString());
}
private Patient createPatient(String theFirstName, String theLastName){
private Patient createPatient(String theFirstName, String theLastName) {
Patient patient = new Patient();
patient.addName().addGiven(theFirstName).setFamily(theLastName);
return (Patient) myPatientDao.create(patient, mySrd).getResource();
@ -1907,7 +1892,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
return (Patient) myPatientDao.create(patient, mySrd).getResource();
}
private Bundle createDocumentBundle(Patient thePatient){
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());
@ -1919,13 +1904,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
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);
@ -1940,10 +1918,86 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
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();
private void assertReadByIdAllowed(IIdType theId, boolean theShouldAllow) {
if (theShouldAllow) {
IBaseResource resource = myClient.read()
.resource(theId.getResourceType())
.withId(theId.toUnqualifiedVersionless().getValue())
.execute();
assertNotNull(resource);
return;
}
try {
myClient.read()
.resource(theId.getResourceType())
.withId(theId.toUnqualifiedVersionless().getValue())
.execute();
fail();
} catch (Exception e) {
assertTrue(e.getMessage().contains("HTTP 403 Forbidden"));
}
}
private void assertTransactionAllowed(Bundle theBundle, boolean theShouldAllow) {
Bundle outcome = myClient.transaction().withBundle(theBundle).execute();
assertNotNull(outcome);
if (theShouldAllow) {
assertNotNull(outcome.getEntry().get(0).getResource());
} else {
assertNull(outcome.getEntry().get(0).getResource());
assertTrue(outcome.getEntry().get(0).getResponse().getStatus().contains("403 Forbidden"));
}
}
private Bundle assertSearchAllowed(String theUrl, boolean theShouldAllow) {
if (theShouldAllow) {
Bundle outcome = myClient.search()
.byUrl(theUrl)
.count(1)
.returnBundle(Bundle.class)
.execute();
assertNotNull(outcome);
assertNotNull(outcome.getEntry().get(0).getResource());
return outcome;
}
try {
myClient.search()
.byUrl(theUrl)
.count(1)
.returnBundle(Bundle.class)
.execute();
fail();
} catch (Exception e) {
assertTrue(e.getMessage().contains("HTTP 403 Forbidden"));
}
return null;
}
private void assertGetNextPageAllowed(Bundle theBundle, boolean theShouldAllow) {
if (theShouldAllow) {
Bundle nextResult = myClient.loadPage()
.next(theBundle)
.execute();
assertNotNull(nextResult);
return;
}
try {
myClient.loadPage()
.next(theBundle)
.execute();
fail();
} catch (Exception e) {
assertTrue(e.getMessage().contains("HTTP 403 Forbidden"));
}
}
private void assertSearchContainsResources(String theUrl, Resource... theExpectedResources) {
List<String> expectedIds = Arrays.stream(theExpectedResources).map(Resource::getIdPart).toList();
Bundle searchResult = myClient
.search()
@ -1959,23 +2013,16 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
assertTrue(expectedIds.containsAll(actualIds));
}
private void assertSearchFailsWith403Forbidden(String theUrl){
private void assertSearchFailsWith403Forbidden(String theUrl) {
try {
myClient.search().byUrl(theUrl).execute();
fail();
} catch (Exception e){
} 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;
}
private Parameters createPatientBirthdatePatch(DateType theNewBirthDate){
private Parameters createPatientBirthdatePatch(DateType theNewBirthDate) {
final Parameters patch = new Parameters();
final Parameters.ParametersParameterComponent op = patch.addParameter().setName("operation");
@ -1986,20 +2033,33 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
return patch;
}
static class ReadAllAuthorizationInterceptor extends AuthorizationInterceptor {
static class ReadAllOfTypeAndTransactionAuthorizationInterceptor extends ReadAllOfTypeAuthorizationInterceptor {
private final String myResourceType;
ReadAllOfTypeAndTransactionAuthorizationInterceptor(String... theResourceTypes) {
super(theResourceTypes);
}
public ReadAllAuthorizationInterceptor(String theResourceType){
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
List<IAuthRule> rules = new ArrayList<>(super.buildRuleList(theRequestDetails));
List<IAuthRule> rulesToAdd = new RuleBuilder().allow().transaction().withAnyOperation().andApplyNormalRules().build();
rules.addAll(rulesToAdd);
return rules;
}
}
static class ReadAllOfTypeAuthorizationInterceptor extends AuthorizationInterceptor {
private final String[] myResourceTypes;
public ReadAllOfTypeAuthorizationInterceptor(String... theResourceTypes) {
super(PolicyEnum.DENY);
myResourceType = theResourceType;
myResourceTypes = theResourceTypes;
}
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().read().resourcesOfType(myResourceType).withAnyId().andThen()
.build();
return Arrays.stream(myResourceTypes).map(resourceType -> new RuleBuilder()
.allow().read().resourcesOfType(resourceType).withAnyId().andThen()
.build()).flatMap(Collection::stream).toList();
}
}
@ -2008,7 +2068,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
private final String myResourceType;
private final IIdType myId;
public ReadInCompartmentAuthorizationInterceptor(String theResourceType, IIdType theId){
public ReadInCompartmentAuthorizationInterceptor(String theResourceType, IIdType theId) {
super(PolicyEnum.DENY);
myResourceType = theResourceType;
myId = theId;
@ -2024,7 +2084,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
static class WriteResourcesInTransactionAuthorizationInterceptor extends AuthorizationInterceptor {
public WriteResourcesInTransactionAuthorizationInterceptor(){
public WriteResourcesInTransactionAuthorizationInterceptor() {
super(PolicyEnum.DENY);
}

View File

@ -80,11 +80,10 @@ public class AuthorizationInterceptor implements IRuleApplier {
public static final String REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS =
AuthorizationInterceptor.class.getName() + "_BulkDataExportOptions";
public static final String BUNDLE = "Bundle";
private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
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);
Set.of(BundleTypeEnum.DOCUMENT, BundleTypeEnum.MESSAGE);
private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
private final String myRequestSeenResourcesKey =
@ -531,7 +530,7 @@ public class AuthorizationInterceptor implements IRuleApplier {
case EXTENDED_OPERATION_TYPE:
case EXTENDED_OPERATION_INSTANCE: {
if (theResponseObject != null) {
resources = toListOfResourcesAndExcludeContainer(theRequestDetails, theResponseObject, fhirContext);
resources = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext);
}
break;
}
@ -578,22 +577,15 @@ public class AuthorizationInterceptor implements IRuleApplier {
OUT,
}
public static List<IBaseResource> toListOfResourcesAndExcludeContainer(
RequestDetails theRequestDetails, IBaseResource theResponseObject, FhirContext fhirContext) {
protected static List<IBaseResource> toListOfResourcesAndExcludeContainer(
IBaseResource theResponseObject, FhirContext fhirContext) {
if (theResponseObject == null) {
return Collections.emptyList();
}
List<IBaseResource> retVal;
boolean shouldExamineChildResources = false;
if (theResponseObject instanceof IBaseBundle) {
IBaseBundle bundle = (IBaseBundle) theResponseObject;
shouldExamineChildResources = shouldExamineBundleChildResources(theRequestDetails, fhirContext, bundle);
} else if (theResponseObject instanceof IBaseParameters) {
shouldExamineChildResources = true;
}
boolean shouldExamineChildResources = shouldExamineChildResources(theResponseObject, fhirContext);
if (!shouldExamineChildResources) {
return Collections.singletonList(theResponseObject);
}
@ -601,7 +593,7 @@ public class AuthorizationInterceptor implements IRuleApplier {
retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class);
// Exclude the container
if (retVal.size() > 0 && retVal.get(0) == theResponseObject) {
if (!retVal.isEmpty() && retVal.get(0) == theResponseObject) {
retVal = retVal.subList(1, retVal.size());
}
@ -612,23 +604,22 @@ public class AuthorizationInterceptor implements IRuleApplier {
}
/**
* 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.
* This method determines if the given Resource should have permissions applied to the resources inside or
* to the Resource itself.
* For Parameters resources, we include child resources when checking the permissions.
* For Bundle resources, we only look at 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) {
protected static boolean shouldExamineChildResources(IBaseResource theResource, FhirContext theFhirContext) {
if (theResource instanceof IBaseParameters) {
return true;
}
BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, theBundle);
boolean isStandaloneBundleResource =
bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType);
return !isStandaloneBundleResource;
if (theResource instanceof IBaseBundle) {
BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, ((IBaseBundle) theResource));
boolean isStandaloneBundleResource =
bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType);
return !isStandaloneBundleResource;
}
return false;
}
public static class Verdict {

View File

@ -832,7 +832,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
} else if (theOutputResource != null) {
List<IBaseResource> outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(
theRequestDetails, theOutputResource, theRequestDetails.getFhirContext());
theOutputResource, theRequestDetails.getFhirContext());
Verdict verdict = null;
for (IBaseResource nextResource : outputResources) {

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
@ -68,6 +69,7 @@ import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CarePlan;
import org.hl7.fhir.r4.model.CodeableConcept;
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.Consent;
import org.hl7.fhir.r4.model.Device;
@ -89,6 +91,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -204,7 +208,7 @@ public class AuthorizationInterceptorR4Test extends BaseValidationTestWithInline
return retVal;
}
private Resource createPatient(Integer theId) {
private Patient createPatient(Integer theId) {
Patient retVal = new Patient();
if (theId != null) {
retVal.setId(new IdType("Patient", (long) theId));
@ -213,8 +217,8 @@ public class AuthorizationInterceptorR4Test extends BaseValidationTestWithInline
return retVal;
}
private Resource createPatient(Integer theId, int theVersion) {
Resource retVal = createPatient(theId);
private Patient createPatient(Integer theId, int theVersion) {
Patient retVal = createPatient(theId);
retVal.setId(retVal.getIdElement().withVersion(Integer.toString(theVersion)));
return retVal;
}
@ -4222,6 +4226,59 @@ public class AuthorizationInterceptorR4Test extends BaseValidationTestWithInline
assertTrue(ourHitMethod);
}
@Test
public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingDocumentBundles_onlyRecursesOneLevelDeep() {
Patient patient = createPatient(1);
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(new Composition());
bundle.addEntry().setResource(patient);
Bundle searchSet = new Bundle();
searchSet.setType(Bundle.BundleType.SEARCHSET);
searchSet.addEntry().setResource(bundle);
RequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setResourceName("Bundle");
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(searchSet, ourCtx);
assertEquals(1, resources.size());
assertTrue(resources.contains(bundle));
}
@Test
public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingPatients_returnsPatients() {
Patient patient1 = createPatient(1);
Patient patient2 = createPatient(2);
Bundle searchSet = new Bundle();
searchSet.setType(Bundle.BundleType.SEARCHSET);
searchSet.addEntry().setResource(patient1);
searchSet.addEntry().setResource(patient2);
RequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setResourceName("Patient");
List<IBaseResource> resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(searchSet, ourCtx);
assertEquals(2, resources.size());
assertTrue(resources.contains(patient1));
assertTrue(resources.contains(patient2));
}
@ParameterizedTest
@EnumSource(value = Bundle.BundleType.class, names = {"DOCUMENT", "MESSAGE"})
public void testShouldExamineBundleResources_withBundleRequestAndStandAloneBundleType_returnsFalse(Bundle.BundleType theBundleType) {
Bundle bundle = new Bundle();
bundle.setType(theBundleType);
assertFalse(AuthorizationInterceptor.shouldExamineChildResources(bundle, ourCtx));
}
@ParameterizedTest
@EnumSource(value = Bundle.BundleType.class, names = {"DOCUMENT", "MESSAGE"}, mode= EnumSource.Mode.EXCLUDE)
public void testShouldExamineBundleResources_withBundleRequestAndNonStandAloneBundleType_returnsTrue(Bundle.BundleType theBundleType) {
Bundle bundle = new Bundle();
bundle.setType(theBundleType);
assertTrue(AuthorizationInterceptor.shouldExamineChildResources(bundle, ourCtx));
}
@AfterAll
public static void afterClassClearContext() throws Exception {
TestUtil.randomizeLocaleAndTimezone();