1
0
mirror of https://github.com/hapifhir/hapi-fhir.git synced 2025-03-28 10:58:47 +00:00

Fix compartment defs for R5 ()

* Fix compartment defs for R5

* Add changelog

* Test fix
This commit is contained in:
James Agnew 2021-04-08 05:59:05 -04:00 committed by GitHub
parent 7cf061d76b
commit 462d9bc6c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 231 additions and 40 deletions
hapi-fhir-base/src/main/java/ca/uhn/fhir/context
hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0
hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa

@ -187,6 +187,11 @@ public class RuntimeResourceDefinition extends BaseRuntimeElementCompositeDefini
for (RuntimeSearchParam next : searchParams) {
if (next.getProvidesMembershipInCompartments() != null) {
for (String nextCompartment : next.getProvidesMembershipInCompartments()) {
if (nextCompartment.startsWith("Base FHIR compartment definition for ")) {
nextCompartment = nextCompartment.substring("Base FHIR compartment definition for ".length());
}
if (!compartmentNameToSearchParams.containsKey(nextCompartment)) {
compartmentNameToSearchParams.put(nextCompartment, new ArrayList<>());
}

@ -0,0 +1,5 @@
---
type: fix
issue: 2528
title: "An issue with compartment definitions in R5 models was fixed. This issue caused some authorization
rules to reject valid requests. Thanks to Patrick Palacin for reporting!"

@ -49,7 +49,6 @@ import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import ca.uhn.fhir.jpa.interceptor.PerformanceTracingLoggingInterceptor;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
@ -61,7 +60,6 @@ import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.jpa.term.TermDeferredStorageSvcImpl;
import ca.uhn.fhir.jpa.term.ValueSetExpansionR4Test;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvcR5;
@ -73,6 +71,7 @@ import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.BasePagingProvider;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ValidationResult;
@ -81,6 +80,7 @@ import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r5.model.AllergyIntolerance;
import org.hl7.fhir.r5.model.Appointment;
import org.hl7.fhir.r5.model.AuditEvent;
@ -164,10 +164,9 @@ import static org.mockito.Mockito.mock;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestR5Config.class})
public abstract class BaseJpaR5Test extends BaseJpaTest {
public abstract class BaseJpaR5Test extends BaseJpaTest implements ITestDataBuilder {
private static IValidationSupport ourJpaValidationSupportChainR5;
private static IFhirResourceDaoValueSet<ValueSet, Coding, CodeableConcept> ourValueSetDao;
@Autowired
protected ITermCodeSystemStorageSvc myTermCodeSystemStorageSvc;
@Autowired
@ -416,6 +415,23 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
@Autowired
private IBulkDataExportSvc myBulkDataExportSvc;
@Override
public IIdType doCreateResource(IBaseResource theResource) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
return dao.create(theResource, mySrd).getId().toUnqualifiedVersionless();
}
@Override
public IIdType doUpdateResource(IBaseResource theResource) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
return dao.update(theResource, mySrd).getId().toUnqualifiedVersionless();
}
@Override
public FhirContext getFhirContext() {
return myFhirCtx;
}
@AfterEach()
public void afterCleanupDao() {
myDaoConfig.setExpireSearchResults(new DaoConfig().isExpireSearchResults());
@ -463,7 +479,7 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
@BeforeEach
public void beforeFlushFT() {
runInTransaction(() -> {
SearchSession searchSession = Search.session(myEntityManager);
SearchSession searchSession = Search.session(myEntityManager);
searchSession.workspace(ResourceTable.class).purge();
// searchSession.workspace(ResourceIndexedSearchParamString.class).purge();
searchSession.indexingPlan().execute();
@ -524,6 +540,43 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
dao.update(resourceParsed);
}
protected ValueSet.ValueSetExpansionContainsComponent assertExpandedValueSetContainsConcept(ValueSet theValueSet, String theSystem, String theCode, String theDisplay, Integer theDesignationCount) {
List<ValueSet.ValueSetExpansionContainsComponent> contains = theValueSet.getExpansion().getContains();
Stream<ValueSet.ValueSetExpansionContainsComponent> stream = contains.stream();
if (theSystem != null) {
stream = stream.filter(concept -> theSystem.equalsIgnoreCase(concept.getSystem()));
}
if (theCode != null) {
stream = stream.filter(concept -> theCode.equalsIgnoreCase(concept.getCode()));
}
if (theDisplay != null) {
stream = stream.filter(concept -> theDisplay.equalsIgnoreCase(concept.getDisplay()));
}
if (theDesignationCount != null) {
stream = stream.filter(concept -> concept.getDesignation().size() == theDesignationCount);
}
Optional<ValueSet.ValueSetExpansionContainsComponent> first = stream.findFirst();
if (!first.isPresent()) {
String failureMessage = String.format("Expanded ValueSet %s did not contain concept [%s|%s|%s] with [%d] designations", theValueSet.getId(), theSystem, theCode, theDisplay, theDesignationCount);
fail(failureMessage);
return null;
} else {
return first.get();
}
}
public List<String> getExpandedConceptsByValueSetUrl(String theValuesetUrl) {
return runInTransaction(() -> {
List<TermValueSet> valueSets = myTermValueSetDao.findTermValueSetByUrl(Pageable.unpaged(), theValuesetUrl);
assertEquals(1, valueSets.size());
TermValueSet valueSet = valueSets.get(0);
List<TermValueSetConcept> concepts = valueSet.getConcepts();
return concepts.stream().map(concept -> concept.getCode()).collect(Collectors.toList());
});
}
@AfterAll
public static void afterClassClearContextBaseJpaR5Test() {
ourValueSetDao.purgeCaches();
@ -634,40 +687,5 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
String[] uuidParams = params.get(Constants.PARAM_PAGINGACTION);
return uuidParams[0];
}
protected ValueSet.ValueSetExpansionContainsComponent assertExpandedValueSetContainsConcept(ValueSet theValueSet, String theSystem, String theCode, String theDisplay, Integer theDesignationCount) {
List<ValueSet.ValueSetExpansionContainsComponent> contains = theValueSet.getExpansion().getContains();
Stream<ValueSet.ValueSetExpansionContainsComponent> stream = contains.stream();
if (theSystem != null) {
stream = stream.filter(concept -> theSystem.equalsIgnoreCase(concept.getSystem()));
}
if (theCode != null ) {
stream = stream.filter(concept -> theCode.equalsIgnoreCase(concept.getCode()));
}
if (theDisplay != null){
stream = stream.filter(concept -> theDisplay.equalsIgnoreCase(concept.getDisplay()));
}
if (theDesignationCount != null) {
stream = stream.filter(concept -> concept.getDesignation().size() == theDesignationCount);
}
Optional<ValueSet.ValueSetExpansionContainsComponent> first = stream.findFirst();
if (!first.isPresent()) {
String failureMessage = String.format("Expanded ValueSet %s did not contain concept [%s|%s|%s] with [%d] designations", theValueSet.getId(), theSystem, theCode, theDisplay, theDesignationCount);
fail(failureMessage);
return null;
} else {
return first.get();
}
}
public List<String> getExpandedConceptsByValueSetUrl(String theValuesetUrl) {
return runInTransaction(() -> {
List<TermValueSet> valueSets = myTermValueSetDao.findTermValueSetByUrl(Pageable.unpaged(), theValuesetUrl);
assertEquals(1, valueSets.size());
TermValueSet valueSet = valueSets.get(0);
List<TermValueSetConcept> concepts = valueSet.getConcepts();
return concepts.stream().map(concept -> concept.getCode()).collect(Collectors.toList());
});
}
}

@ -0,0 +1,163 @@
package ca.uhn.fhir.jpa.provider.r5;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.provider.r5.BaseResourceProviderR5Test;
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.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.ResourceGoneException;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRuleTester;
import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum;
import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
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.r5.model.Bundle;
import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Condition;
import org.hl7.fhir.r5.model.Encounter;
import org.hl7.fhir.r5.model.Enumerations;
import org.hl7.fhir.r5.model.IdType;
import org.hl7.fhir.r5.model.Identifier;
import org.hl7.fhir.r5.model.Observation;
import org.hl7.fhir.r5.model.Organization;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.Patient;
import org.hl7.fhir.r5.model.Practitioner;
import org.hl7.fhir.r5.model.Reference;
import org.hl7.fhir.r5.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
public class AuthorizationInterceptorJpaR5Test extends BaseResourceProviderR5Test {
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptorJpaR5Test.class);
@BeforeEach
@Override
public void before() throws Exception {
super.before();
myDaoConfig.setAllowMultipleDelete(true);
myDaoConfig.setExpungeEnabled(true);
myDaoConfig.setDeleteExpungeEnabled(true);
}
/**
* See #503
*/
@Test
public void testDeleteIsAllowedForCompartment() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad");
final IIdType id = myClient.create().resource(patient).execute().getId();
Observation obsInCompartment = new Observation();
obsInCompartment.setStatus(Enumerations.ObservationStatus.FINAL);
obsInCompartment.getSubject().setReferenceElement(id.toUnqualifiedVersionless());
IIdType obsInCompartmentId = myClient.create().resource(obsInCompartment).execute().getId().toUnqualifiedVersionless();
Observation obsNotInCompartment = new Observation();
obsNotInCompartment.setStatus(Enumerations.ObservationStatus.FINAL);
IIdType obsNotInCompartmentId = myClient.create().resource(obsNotInCompartment).execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().delete().resourcesOfType(Observation.class).inCompartment("Patient", id).andThen()
.deny().delete().allResources().withAnyId().andThen()
.allowAll()
.build();
}
});
myClient.delete().resourceById(obsInCompartmentId.toUnqualifiedVersionless()).execute();
try {
myClient.delete().resourceById(obsNotInCompartmentId.toUnqualifiedVersionless()).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
@Test
public void testDeleteIsAllowedForCompartmentUsingTransaction() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad");
final IIdType id = myClient.create().resource(patient).execute().getId();
Observation obsInCompartment = new Observation();
obsInCompartment.setStatus(Enumerations.ObservationStatus.FINAL);
obsInCompartment.getSubject().setReferenceElement(id.toUnqualifiedVersionless());
IIdType obsInCompartmentId = myClient.create().resource(obsInCompartment).execute().getId().toUnqualifiedVersionless();
Observation obsNotInCompartment = new Observation();
obsNotInCompartment.setStatus(Enumerations.ObservationStatus.FINAL);
IIdType obsNotInCompartmentId = myClient.create().resource(obsNotInCompartment).execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().delete().resourcesOfType(Observation.class).inCompartment("Patient", id).andThen()
.allow().transaction().withAnyOperation().andApplyNormalRules().andThen()
.denyAll()
.build();
}
});
Bundle bundle;
bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl(obsInCompartmentId.toUnqualifiedVersionless().getValue());
myClient.transaction().withBundle(bundle).execute();
try {
bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.DELETE).setUrl(obsNotInCompartmentId.toUnqualifiedVersionless().getValue());
myClient.transaction().withBundle(bundle).execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
}