5558 disallow resource updates crossing patient compartment (#5562)
* Improve readability * Fix warnings * Add interceptor, test doc and changelog. Also, extract common used function to utility class and add tests. * spotless * Fix changelog link * Fix changelog fragment warnings * Fix changelog link * Fix documentation link * Implement review comments * Implement review comments * Keep old exception code for tests * Revert refactoring which lost difference between NonCompartmentMemberTypeResponse and NonCompartmentMemberInstanceResponse * spotless * Remove version restriction * Differentiate exceptions thrown by different code --------- Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
parent
0785411be4
commit
836b755ad3
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: add
|
||||
issue: 5558
|
||||
title: "An interceptor was added to block resource updates which would cause the resource to change Patient compartment.
|
||||
Please see [JPA Server: Block Resource Updates Changing Patient Compartment](/hapi-fhir/docs/interceptors/built_in_server_interceptors.html#jpa-server-block-resource-updates-changing-patient-compartment)
|
||||
for more information."
|
|
@ -305,7 +305,14 @@ The CascadingDeleteInterceptor allows clients to request deletes be cascaded to
|
|||
The OverridePathBasedReferentialIntegrityForDeletesInterceptor can be registered and configured to allow resources to be deleted even if other resources have outgoing references to the deleted resource. While it is generally a bad idea to allow deletion of resources that are referred to from other resources, there are circumstances where it is desirable. For example, if you have Provenance or AuditEvent resources that refer to a Patient resource that was created in error, you might want to alow the Patient to be deleted while leaving the Provenance and AuditEvent resources intact (including the now-invalid outgoing references to that Patient).
|
||||
|
||||
This interceptor uses FHIRPath expressions to indicate the resource paths that should not have referential integrity applied to them. For example, if this interceptor is configured with a path of `AuditEvent.agent.who`, a Patient resource would be allowed to be deleted even if one or more AuditEvents had references in that path to the given Patient (unless other resources also had references to the Patient).
|
||||
|
||||
|
||||
# JPA Server: Block Resource Updates Changing Patient Compartment
|
||||
|
||||
* [PatientCompartmentEnforcingInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptor.java)
|
||||
|
||||
The PatientCompartmentEnforcingInterceptor can be registered to block resource updates which would change the resource's patient compartment. This interceptor requires FHIR version R4 or later.
|
||||
|
||||
If the JPA server has [partitioning](/docs/server_jpa_partitioning/partitioning.html) enabled, and [Tenant Identification Strategy](/docs/server_plain/multitenancy.html) is PATIENT_ID, the PatientCompartmentEnforcingInterceptor can be used to block resources to change partition as a consequence of updating a patient reference.
|
||||
|
||||
# JPA Server: Retry on Version Conflicts
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Changelog: 2023
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2023', '')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2023', '')}"/>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Changelog: 2014
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2014', '2014')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2014', '2014')}"/>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Changelog: 2015
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2015', '2015')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2015', '2015')}"/>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Changelog: 2016
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2016', '2016')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2016', '2016')}"/>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Changelog: 2017
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2017', '2017')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2017', '2017')}"/>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Changelog: 2018
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2018', '2018')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2018', '2018')}"/>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Changelog: 2019
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2019', '2019')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2019', '2019')}"/>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
# Changelog: 2020
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2020', '2020')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2020', '2020')}"/>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
# Changelog: 2021
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2021', '2021')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2021', '2021')}"/>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
# Changelog: 2022
|
||||
|
||||
<th:block th:insert="fragment_changelog.md :: changelog('2022', '2022')"/>
|
||||
<th:block th:insert="~{fragment_changelog.md :: changelog('2022', '2022')}"/>
|
||||
|
||||
|
|
|
@ -176,4 +176,22 @@ public class PartitionSettings {
|
|||
*/
|
||||
ALLOWED_UNQUALIFIED,
|
||||
}
|
||||
|
||||
public enum BlockPatientCompartmentUpdateMode {
|
||||
/**
|
||||
* Resource updates which would change resource's patient compartment are blocked.
|
||||
*/
|
||||
ALWAYS,
|
||||
|
||||
/**
|
||||
* Resource updates which would change resource's patient compartment are blocked
|
||||
* when Partition Selection Mode is PATIENT_ID
|
||||
*/
|
||||
DEFAULT,
|
||||
|
||||
/**
|
||||
* Resource updates which would change resource's patient compartment are allowed.
|
||||
*/
|
||||
NEVER,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
package ca.uhn.fhir.jpa.interceptor;
|
||||
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hl7.fhir.r4.model.Annotation;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class PatientCompartmentEnforcingInterceptorTest extends BaseJpaR4Test {
|
||||
|
||||
public static final int ALTERNATE_DEFAULT_ID = -1;
|
||||
private PatientIdPartitionInterceptor mySvc;
|
||||
private ForceOffsetSearchModeInterceptor myForceOffsetSearchModeInterceptor;
|
||||
private PatientCompartmentEnforcingInterceptor myUpdateCrossPartitionInterceptor;
|
||||
|
||||
@Autowired
|
||||
private ISearchParamExtractor mySearchParamExtractor;
|
||||
|
||||
@Override
|
||||
@BeforeEach
|
||||
public void before() throws Exception {
|
||||
super.before();
|
||||
mySvc = new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings);
|
||||
myForceOffsetSearchModeInterceptor = new ForceOffsetSearchModeInterceptor();
|
||||
myUpdateCrossPartitionInterceptor = new PatientCompartmentEnforcingInterceptor(
|
||||
myFhirContext, mySearchParamExtractor);
|
||||
|
||||
myInterceptorRegistry.registerInterceptor(mySvc);
|
||||
myInterceptorRegistry.registerInterceptor(myForceOffsetSearchModeInterceptor);
|
||||
myInterceptorRegistry.registerInterceptor(myUpdateCrossPartitionInterceptor);
|
||||
|
||||
myPartitionSettings.setPartitioningEnabled(true);
|
||||
myPartitionSettings.setUnnamedPartitionMode(true);
|
||||
myPartitionSettings.setDefaultPartitionId(ALTERNATE_DEFAULT_ID);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
myInterceptorRegistry.unregisterInterceptor(mySvc);
|
||||
myInterceptorRegistry.unregisterInterceptor(myForceOffsetSearchModeInterceptor);
|
||||
myInterceptorRegistry.unregisterInterceptor(myUpdateCrossPartitionInterceptor);
|
||||
|
||||
myPartitionSettings.setPartitioningEnabled(false);
|
||||
myPartitionSettings.setUnnamedPartitionMode(new PartitionSettings().isUnnamedPartitionMode());
|
||||
myPartitionSettings.setDefaultPartitionId(new PartitionSettings().getDefaultPartitionId());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUpdateResource_whenCrossingPatientCompartment_throws() {
|
||||
createPatientA();
|
||||
createPatientB();
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getSubject().setReference("Patient/A");
|
||||
myObservationDao.create(obs, new SystemRequestDetails());
|
||||
|
||||
// try updating observation's patient, which would cross partition boundaries
|
||||
obs.getSubject().setReference("Patient/B");
|
||||
|
||||
InternalErrorException thrown = assertThrows(InternalErrorException.class, () -> myObservationDao.update(obs, new SystemRequestDetails()));
|
||||
assertEquals("HAPI-2476: Resource compartment changed. Was a referenced Patient changed?", thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateResource_whenNotCrossingPatientCompartment_allows() {
|
||||
createPatientA();
|
||||
|
||||
Observation obs = new Observation();
|
||||
obs.getSubject().setReference("Patient/A");
|
||||
myObservationDao.create(obs, new SystemRequestDetails());
|
||||
|
||||
obs.getNote().add(new Annotation().setText("some text"));
|
||||
obs.setStatus(Observation.ObservationStatus.CORRECTED);
|
||||
|
||||
myObservationDao.update(obs, new SystemRequestDetails());
|
||||
}
|
||||
|
||||
private void createPatientA() {
|
||||
Patient patient = new Patient();
|
||||
patient.setId("Patient/A");
|
||||
patient.setActive(true);
|
||||
myPatientDao.update(patient, new SystemRequestDetails());
|
||||
}
|
||||
|
||||
private void createPatientB() {
|
||||
Patient patient = new Patient();
|
||||
patient.setId("Patient/B");
|
||||
patient.setActive(true);
|
||||
myPatientDao.update(patient, new SystemRequestDetails());
|
||||
}
|
||||
|
||||
}
|
|
@ -5,8 +5,6 @@ import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider;
|
|||
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.bulk.export.model.BulkExportResponseJson;
|
||||
|
@ -14,7 +12,6 @@ import ca.uhn.fhir.jpa.entity.PartitionEntity;
|
|||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
|
@ -29,12 +26,12 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
|||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import ca.uhn.fhir.util.JsonUtil;
|
||||
import com.google.common.collect.Sets;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.CapabilityStatement;
|
||||
import org.hl7.fhir.r4.model.Condition;
|
||||
|
@ -42,7 +39,6 @@ import org.hl7.fhir.r4.model.IdType;
|
|||
import org.hl7.fhir.r4.model.Organization;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Resource;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
|
@ -51,13 +47,10 @@ import org.junit.jupiter.params.ParameterizedTest;
|
|||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
|
@ -109,9 +102,10 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
createPatient(withTenant(TENANT_B), withActiveFalse());
|
||||
|
||||
runInTransaction(() -> {
|
||||
PartitionEntity partition = myPartitionDao.findForName(TENANT_A).orElseThrow(() -> new IllegalStateException());
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
assertEquals(partition.getId(), resourceTable.getPartitionId().getPartitionId());
|
||||
PartitionEntity partition = myPartitionDao.findForName(TENANT_A).orElseThrow(IllegalStateException::new);
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assert resourceTable.getPartitionId() != null;
|
||||
assertEquals(partition.getId(), resourceTable.getPartitionId().getPartitionId());
|
||||
});
|
||||
|
||||
// Now read back
|
||||
|
@ -147,7 +141,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
createPatient(withTenant(TENANT_B), withActiveFalse());
|
||||
|
||||
runInTransaction(() -> {
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assertNull(resourceTable.getPartitionId());
|
||||
});
|
||||
|
||||
|
@ -187,7 +181,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeIncludeDeleted(
|
||||
"Patient", Arrays.asList(patientId)
|
||||
"Patient", List.of(patientId)
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -195,7 +189,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoType(
|
||||
"Patient", Arrays.asList(patientId), true
|
||||
"Patient", List.of(patientId), true
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -206,7 +200,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeIncludeDeleted(
|
||||
"Patient", Arrays.asList(patientId)
|
||||
"Patient", List.of(patientId)
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -214,7 +208,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoType(
|
||||
"Patient", Arrays.asList(patientId), true
|
||||
"Patient", List.of(patientId), true
|
||||
);
|
||||
assertThat(forcedIds, hasSize(0));
|
||||
});
|
||||
|
@ -229,7 +223,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(
|
||||
"Patient", Arrays.asList(patientId), false
|
||||
"Patient", List.of(patientId), false
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -237,7 +231,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(
|
||||
"Patient", Arrays.asList(patientId), true
|
||||
"Patient", List.of(patientId), true
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -248,7 +242,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(
|
||||
"Patient", Arrays.asList(patientId), false
|
||||
"Patient", List.of(patientId), false
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -256,7 +250,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(
|
||||
"Patient", Arrays.asList(patientId), true
|
||||
"Patient", List.of(patientId), true
|
||||
);
|
||||
assertEquals(0, forcedIds.size());
|
||||
});
|
||||
|
@ -273,7 +267,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID), false
|
||||
"Patient", List.of(patientId), List.of(TENANT_A_ID), false
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -281,7 +275,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID), true
|
||||
"Patient", List.of(patientId), List.of(TENANT_A_ID), true
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -291,7 +285,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID), false
|
||||
"Patient", List.of(patientId), List.of(TENANT_A_ID), false
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -299,14 +293,14 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID), true
|
||||
"Patient", List.of(patientId), List.of(TENANT_A_ID), true
|
||||
);
|
||||
assertEquals(0, forcedIds.size());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindAndResolveByForcedIdWithNoTypeInPartition() throws IOException {
|
||||
public void testFindAndResolveByForcedIdWithNoTypeInPartition() {
|
||||
// Create patients
|
||||
String patientId = "AAA";
|
||||
IIdType idA = createPatient(withTenant(TENANT_A), withId(patientId));
|
||||
|
@ -316,7 +310,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartition(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), false
|
||||
"Patient", List.of(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), false
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -324,7 +318,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartition(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), true
|
||||
"Patient", List.of(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), true
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -334,7 +328,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and include deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartition(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), false
|
||||
"Patient", List.of(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), false
|
||||
);
|
||||
assertContainsSingleForcedId(forcedIds, patientId);
|
||||
});
|
||||
|
@ -342,7 +336,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
// Search and filter deleted
|
||||
runInTransaction(() -> {
|
||||
Collection<Object[]> forcedIds = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartition(
|
||||
"Patient", Arrays.asList(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), true
|
||||
"Patient", List.of(patientId), Arrays.asList(TENANT_A_ID, TENANT_B_ID), true
|
||||
);
|
||||
assertEquals(0, forcedIds.size());
|
||||
});
|
||||
|
@ -367,7 +361,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
IIdType idA = createResource("NamingSystem", withTenant(JpaConstants.DEFAULT_PARTITION_NAME), withStatus("draft"));
|
||||
|
||||
runInTransaction(() -> {
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assertNull(resourceTable.getPartitionId());
|
||||
});
|
||||
|
||||
|
@ -421,10 +415,12 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
IdType idB = new IdType(response.getEntry().get(1).getResponse().getLocation());
|
||||
|
||||
runInTransaction(() -> {
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assert resourceTable.getPartitionId() != null;
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assert resourceTable.getPartitionId() != null;
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -493,10 +489,11 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
IIdType idB = myPatientDao.create((Patient) patientB, requestDetails).getId();
|
||||
|
||||
runInTransaction(() -> {
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assertNull(resourceTable.getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
assertEquals(2, resourceTable.getPartitionId().getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assert resourceTable.getPartitionId() != null;
|
||||
assertEquals(2, resourceTable.getPartitionId().getPartitionId());
|
||||
});
|
||||
|
||||
|
||||
|
@ -556,10 +553,11 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
myPatientDao.update((Patient) patientB, requestDetails);
|
||||
|
||||
runInTransaction(() -> {
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assertNull(resourceTable.getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
assertEquals(2, resourceTable.getPartitionId().getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assert resourceTable.getPartitionId() != null;
|
||||
assertEquals(2, resourceTable.getPartitionId().getPartitionId());
|
||||
});
|
||||
|
||||
|
||||
|
@ -619,10 +617,12 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
IdType idB = new IdType(response.getEntry().get(1).getResponse().getLocation());
|
||||
|
||||
runInTransaction(() -> {
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assert resourceTable.getPartitionId() != null;
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
resourceTable = myResourceTableDao.findById(idB.getIdPartAsLong()).orElseThrow(IllegalStateException::new);
|
||||
assert resourceTable.getPartitionId() != null;
|
||||
assertEquals(1, resourceTable.getPartitionId().getPartitionId());
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -646,7 +646,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testIncludeInTenantWithAssignedID() throws Exception {
|
||||
public void testIncludeInTenantWithAssignedID() {
|
||||
IIdType idA = createResource("Patient", withTenant(JpaConstants.DEFAULT_PARTITION_NAME), withId("test"), withFamily("Smith"), withActiveTrue());
|
||||
createConditionWithAllowedUnqualified(idA);
|
||||
Bundle response = myClient.search().byUrl(myClient.getServerBase() + "/" + TENANT_A + "/Condition?subject=Patient/" + idA.getIdPart() + "&_include=Condition:subject").returnBundle(Bundle.class).execute();
|
||||
|
@ -654,7 +654,7 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testIncludeInTenantWithAutoGeneratedID() throws Exception {
|
||||
public void testIncludeInTenantWithAutoGeneratedID() {
|
||||
IIdType idA = createResource("Patient", withTenant(JpaConstants.DEFAULT_PARTITION_NAME), withFamily("Smith"), withActiveTrue());
|
||||
createConditionWithAllowedUnqualified(idA);
|
||||
Bundle response = myClient.search().byUrl(myClient.getServerBase() + "/" + TENANT_A + "/Condition?subject=Patient/" + idA.getIdPart() + "&_include=Condition:subject").returnBundle(Bundle.class).execute();
|
||||
|
@ -678,25 +678,8 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
@Mock
|
||||
private IJobCoordinator myJobCoordinator;
|
||||
|
||||
@Spy
|
||||
private RequestPartitionHelperSvc myRequestPartitionHelperSvc = new MultitenantServerR4Test.PartitionTesting.MyRequestPartitionHelperSvc();
|
||||
|
||||
String myTenantName = null;
|
||||
|
||||
private class MyRequestPartitionHelperSvc extends RequestPartitionHelperSvc {
|
||||
|
||||
@Override
|
||||
public RequestPartitionId determineReadPartitionForRequest(RequestDetails theRequest, ReadPartitionIdRequestDetails theDetails) {
|
||||
return RequestPartitionId.fromPartitionName(myTenantName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateHasPartitionPermissions(RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkExportForDifferentPartitions() throws IOException {
|
||||
setBulkDataExportProvider();
|
||||
|
@ -745,13 +728,6 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
|
|||
when(servletRequestDetails.getServletResponse())
|
||||
.thenReturn(mockResponse);
|
||||
|
||||
List<IPrimitiveType<String>> filters = new ArrayList<>();
|
||||
if (options.getFilters() != null) {
|
||||
for (String v : options.getFilters()) {
|
||||
filters.add(new StringType(v));
|
||||
}
|
||||
}
|
||||
|
||||
//perform export-poll-status
|
||||
myTenantName = createInPartition;
|
||||
HttpGet get = new HttpGet(buildExportUrl(createInPartition, jobId));
|
||||
|
|
|
@ -166,6 +166,12 @@
|
|||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>${mockito_version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package ca.uhn.fhir.jpa.interceptor;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.util.ResourceCompartmentUtil;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
||||
|
||||
/**
|
||||
* This interceptor can be used to block resource updates which would make resource patient compartment change.
|
||||
* <p/>
|
||||
* This could be used when the JPA server has partitioning enabled, and Tenant Identification Strategy is PATIENT_ID.
|
||||
*/
|
||||
@Interceptor
|
||||
public class PatientCompartmentEnforcingInterceptor {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(PatientCompartmentEnforcingInterceptor.class);
|
||||
|
||||
private final FhirContext myFhirContext;
|
||||
private final ISearchParamExtractor mySearchParamExtractor;
|
||||
|
||||
public PatientCompartmentEnforcingInterceptor(
|
||||
FhirContext theFhirContext, ISearchParamExtractor theSearchParamExtractor) {
|
||||
myFhirContext = theFhirContext;
|
||||
mySearchParamExtractor = theSearchParamExtractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks resource updates which would make the resource change Patient Compartment.
|
||||
* @param theOldResource the original resource state
|
||||
* @param theResource the updated resource state
|
||||
*/
|
||||
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
|
||||
public void storagePreStorageResourceUpdated(IBaseResource theOldResource, IBaseResource theResource) {
|
||||
|
||||
ourLog.info("Interceptor STORAGE_PRESTORAGE_RESOURCE_UPDATED - started");
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
try {
|
||||
String patientCompartmentOld = ResourceCompartmentUtil.getPatientCompartmentIdentity(
|
||||
theOldResource, myFhirContext, mySearchParamExtractor)
|
||||
.orElse(EMPTY);
|
||||
String patientCompartmentCurrent = ResourceCompartmentUtil.getPatientCompartmentIdentity(
|
||||
theResource, myFhirContext, mySearchParamExtractor)
|
||||
.orElse(EMPTY);
|
||||
|
||||
if (!StringUtils.equals(patientCompartmentOld, patientCompartmentCurrent)) {
|
||||
// Avoid disclosing compartments in message, which could have security implications
|
||||
throw new InternalErrorException(
|
||||
Msg.code(2476) + "Resource compartment changed. Was a referenced Patient changed?");
|
||||
}
|
||||
|
||||
} finally {
|
||||
ourLog.info("Interceptor STORAGE_PRESTORAGE_RESOURCE_UPDATED - ended, execution took {}", stopWatch);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,23 +30,21 @@ import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
|
|||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.util.ResourceCompartmentUtil;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
@ -92,40 +90,28 @@ public class PatientIdPartitionInterceptor {
|
|||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
|
||||
public RequestPartitionId identifyForCreate(IBaseResource theResource, RequestDetails theRequestDetails) {
|
||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResource);
|
||||
List<RuntimeSearchParam> compartmentSps = getCompartmentSearchParams(resourceDef);
|
||||
List<RuntimeSearchParam> compartmentSps =
|
||||
ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef);
|
||||
if (compartmentSps.isEmpty()) {
|
||||
return provideNonCompartmentMemberTypeResponse(theResource);
|
||||
}
|
||||
|
||||
String compartmentIdentity;
|
||||
Optional<String> oCompartmentIdentity;
|
||||
if (resourceDef.getName().equals("Patient")) {
|
||||
compartmentIdentity = theResource.getIdElement().getIdPart();
|
||||
if (isBlank(compartmentIdentity)) {
|
||||
oCompartmentIdentity =
|
||||
Optional.ofNullable(theResource.getIdElement().getIdPart());
|
||||
if (oCompartmentIdentity.isEmpty()) {
|
||||
throw new MethodNotAllowedException(
|
||||
Msg.code(1321) + "Patient resource IDs must be client-assigned in patient compartment mode");
|
||||
}
|
||||
} else {
|
||||
compartmentIdentity = compartmentSps.stream()
|
||||
.flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath())))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(path -> mySearchParamExtractor
|
||||
.getPathValueExtractor(theResource, path)
|
||||
.get())
|
||||
.filter(t -> !t.isEmpty())
|
||||
.map(t -> t.get(0))
|
||||
.filter(t -> t instanceof IBaseReference)
|
||||
.map(t -> (IBaseReference) t)
|
||||
.map(t -> t.getReferenceElement().getValue())
|
||||
.map(t -> new IdType(t).getIdPart())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (isBlank(compartmentIdentity)) {
|
||||
return provideNonCompartmentMemberInstanceResponse(theResource);
|
||||
}
|
||||
oCompartmentIdentity =
|
||||
ResourceCompartmentUtil.getResourceCompartment(theResource, compartmentSps, mySearchParamExtractor);
|
||||
}
|
||||
|
||||
return provideCompartmentMemberInstanceResponse(theRequestDetails, compartmentIdentity);
|
||||
return oCompartmentIdentity
|
||||
.map(ci -> provideCompartmentMemberInstanceResponse(theRequestDetails, ci))
|
||||
.orElseGet(() -> provideNonCompartmentMemberInstanceResponse(theResource));
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
|
||||
|
@ -135,7 +121,8 @@ public class PatientIdPartitionInterceptor {
|
|||
return provideNonCompartmentMemberTypeResponse(null);
|
||||
}
|
||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theReadDetails.getResourceType());
|
||||
List<RuntimeSearchParam> compartmentSps = getCompartmentSearchParams(resourceDef);
|
||||
List<RuntimeSearchParam> compartmentSps =
|
||||
ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef);
|
||||
if (compartmentSps.isEmpty()) {
|
||||
return provideNonCompartmentMemberTypeResponse(null);
|
||||
}
|
||||
|
|
|
@ -219,54 +219,53 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition
|
|||
@Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource, @Nonnull String theResourceType) {
|
||||
RequestPartitionId requestPartitionId;
|
||||
|
||||
if (myPartitionSettings.isPartitioningEnabled()) {
|
||||
boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType);
|
||||
|
||||
// TODO GGG eventually, theRequest will not be allowed to be null here, and we will pass through
|
||||
// SystemRequestDetails instead.
|
||||
if ((theRequest == null || theRequest instanceof SystemRequestDetails) && nonPartitionableResource) {
|
||||
return RequestPartitionId.defaultPartition();
|
||||
}
|
||||
|
||||
if (theRequest instanceof SystemRequestDetails
|
||||
&& systemRequestHasExplicitPartition((SystemRequestDetails) theRequest)) {
|
||||
requestPartitionId =
|
||||
getSystemRequestPartitionId((SystemRequestDetails) theRequest, nonPartitionableResource);
|
||||
} else {
|
||||
if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, theRequest)) {
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(
|
||||
myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params);
|
||||
} else {
|
||||
// This is an external Request (e.g. ServletRequestDetails) so we want to figure out the partition
|
||||
// via interceptor.
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
|
||||
HookParams params = new HookParams()
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(
|
||||
myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params);
|
||||
}
|
||||
|
||||
// If the interceptors haven't selected a partition, and its a non-partitionable resource anyhow, send
|
||||
// to DEFAULT
|
||||
if (nonPartitionableResource && requestPartitionId == null) {
|
||||
requestPartitionId = RequestPartitionId.defaultPartition();
|
||||
}
|
||||
}
|
||||
|
||||
String resourceName = myFhirContext.getResourceType(theResource);
|
||||
validateSinglePartitionForCreate(
|
||||
requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE);
|
||||
|
||||
return validateNormalizeAndNotifyHooksForRead(requestPartitionId, theRequest, theResourceType);
|
||||
if (!myPartitionSettings.isPartitioningEnabled()) {
|
||||
return RequestPartitionId.allPartitions();
|
||||
}
|
||||
|
||||
return RequestPartitionId.allPartitions();
|
||||
boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType);
|
||||
|
||||
// TODO GGG eventually, theRequest will not be allowed to be null here, and we will pass through
|
||||
// SystemRequestDetails instead.
|
||||
if ((theRequest == null || theRequest instanceof SystemRequestDetails) && nonPartitionableResource) {
|
||||
return RequestPartitionId.defaultPartition();
|
||||
}
|
||||
|
||||
if (theRequest instanceof SystemRequestDetails
|
||||
&& systemRequestHasExplicitPartition((SystemRequestDetails) theRequest)) {
|
||||
requestPartitionId =
|
||||
getSystemRequestPartitionId((SystemRequestDetails) theRequest, nonPartitionableResource);
|
||||
} else {
|
||||
if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, theRequest)) {
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(
|
||||
myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params);
|
||||
} else {
|
||||
// This is an external Request (e.g. ServletRequestDetails) so we want to figure out the partition
|
||||
// via interceptor.
|
||||
// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
|
||||
HookParams params = new HookParams()
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest);
|
||||
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(
|
||||
myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params);
|
||||
}
|
||||
|
||||
// If the interceptors haven't selected a partition, and its a non-partitionable resource anyhow, send
|
||||
// to DEFAULT
|
||||
if (nonPartitionableResource && requestPartitionId == null) {
|
||||
requestPartitionId = RequestPartitionId.defaultPartition();
|
||||
}
|
||||
}
|
||||
|
||||
String resourceName = myFhirContext.getResourceType(theResource);
|
||||
validateSinglePartitionForCreate(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE);
|
||||
|
||||
return validateNormalizeAndNotifyHooksForRead(requestPartitionId, theRequest, theResourceType);
|
||||
}
|
||||
|
||||
private boolean systemRequestHasExplicitPartition(@Nonnull SystemRequestDetails theRequest) {
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseReference;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
public class ResourceCompartmentUtil {
|
||||
|
||||
/**
|
||||
* Extract, if exists, the patient compartment identity of the received resource.
|
||||
* It must be invoked in patient compartment mode.
|
||||
* @param theResource the resource to which extract the patient compartment identity
|
||||
* @param theFhirContext the active FhirContext
|
||||
* @param theSearchParamExtractor the configured search parameter extractor
|
||||
* @return the optional patient compartment identifier
|
||||
* @throws MethodNotAllowedException if received resource is of type "Patient" and ID is not assigned.
|
||||
*/
|
||||
public static Optional<String> getPatientCompartmentIdentity(
|
||||
IBaseResource theResource, FhirContext theFhirContext, ISearchParamExtractor theSearchParamExtractor) {
|
||||
RuntimeResourceDefinition resourceDef = theFhirContext.getResourceDefinition(theResource);
|
||||
List<RuntimeSearchParam> patientCompartmentSps =
|
||||
ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef);
|
||||
if (patientCompartmentSps.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (resourceDef.getName().equals("Patient")) {
|
||||
String compartmentIdentity = theResource.getIdElement().getIdPart();
|
||||
if (isBlank(compartmentIdentity)) {
|
||||
throw new MethodNotAllowedException(
|
||||
Msg.code(2475) + "Patient resource IDs must be client-assigned in patient compartment mode");
|
||||
}
|
||||
return Optional.of(compartmentIdentity);
|
||||
}
|
||||
|
||||
return getResourceCompartment(theResource, patientCompartmentSps, theSearchParamExtractor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and returns an optional compartment of the received resource
|
||||
* @param theResource source resource which compartment is extracted
|
||||
* @param theCompartmentSps the RuntimeSearchParam list involving the searched compartment
|
||||
* @param mySearchParamExtractor the ISearchParamExtractor to be used to extract the parameter values
|
||||
* @return optional compartment of the received resource
|
||||
*/
|
||||
public static Optional<String> getResourceCompartment(
|
||||
IBaseResource theResource,
|
||||
List<RuntimeSearchParam> theCompartmentSps,
|
||||
ISearchParamExtractor mySearchParamExtractor) {
|
||||
return theCompartmentSps.stream()
|
||||
.flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath())))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(path -> mySearchParamExtractor
|
||||
.getPathValueExtractor(theResource, path)
|
||||
.get())
|
||||
.filter(t -> !t.isEmpty())
|
||||
.map(t -> t.get(0))
|
||||
.filter(t -> t instanceof IBaseReference)
|
||||
.map(t -> (IBaseReference) t)
|
||||
.map(t -> t.getReferenceElement().getValue())
|
||||
.map(t -> new IdType(t).getIdPart())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code RuntimeSearchParam} list with the parameters extracted from the received
|
||||
* {@code RuntimeResourceDefinition}, which are of type REFERENCE and have a membership compartment
|
||||
* for "Patient" resource
|
||||
* @param resourceDef the RuntimeResourceDefinition providing the RuntimeSearchParam list
|
||||
* @return the RuntimeSearchParam filtered list
|
||||
*/
|
||||
@Nonnull
|
||||
public static List<RuntimeSearchParam> getPatientCompartmentSearchParams(RuntimeResourceDefinition resourceDef) {
|
||||
return resourceDef.getSearchParams().stream()
|
||||
.filter(param -> param.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
|
||||
.filter(param -> param.getProvidesMembershipInCompartments() != null
|
||||
&& param.getProvidesMembershipInCompartments().contains("Patient"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ResourceCompartmentUtilTest {
|
||||
|
||||
@Mock
|
||||
private RuntimeResourceDefinition myRuntimeResourceDefinition;
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private IBaseResource myResource;
|
||||
@Mock
|
||||
private FhirContext myFhirContext;
|
||||
private List<RuntimeSearchParam> myCompartmentSearchParams;
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private ISearchParamExtractor mySearchParamExtractor;
|
||||
|
||||
@Test
|
||||
void getResourceCompartment() {
|
||||
myCompartmentSearchParams = getMockSearchParams(true);
|
||||
when(mySearchParamExtractor.getPathValueExtractor(myResource, "Observation.subject"))
|
||||
.thenReturn(() -> List.of(new Reference("Patient/P01")));
|
||||
|
||||
Optional<String> oCompartment = ResourceCompartmentUtil.getResourceCompartment(
|
||||
myResource, myCompartmentSearchParams, mySearchParamExtractor);
|
||||
|
||||
assertTrue(oCompartment.isPresent());
|
||||
assertEquals("P01", oCompartment.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPatientCompartmentSearchParams() {
|
||||
myCompartmentSearchParams = getMockSearchParams(true);
|
||||
when(myRuntimeResourceDefinition.getSearchParams()).thenReturn(myCompartmentSearchParams);
|
||||
|
||||
List<RuntimeSearchParam> result = ResourceCompartmentUtil.getPatientCompartmentSearchParams(myRuntimeResourceDefinition);
|
||||
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class TestGetPatientCompartmentIdentity {
|
||||
@Test
|
||||
void whenNoPatientCompartmentsReturnsEmpty() {
|
||||
myCompartmentSearchParams = getMockSearchParams(false);
|
||||
when(myFhirContext.getResourceDefinition(myResource)).thenReturn(myRuntimeResourceDefinition);
|
||||
when(myRuntimeResourceDefinition.getSearchParams()).thenReturn(myCompartmentSearchParams);
|
||||
|
||||
Optional<String> result = ResourceCompartmentUtil.getPatientCompartmentIdentity(myResource, myFhirContext, mySearchParamExtractor);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenPatientResource_andNoId_throws() {
|
||||
myCompartmentSearchParams = getMockSearchParams(true);
|
||||
when(myFhirContext.getResourceDefinition(myResource)).thenReturn(myRuntimeResourceDefinition);
|
||||
when(myRuntimeResourceDefinition.getSearchParams()).thenReturn(myCompartmentSearchParams);
|
||||
when(myRuntimeResourceDefinition.getName()).thenReturn("Patient");
|
||||
when(myResource.getIdElement().getIdPart()).thenReturn(null);
|
||||
|
||||
MethodNotAllowedException thrown = assertThrows(MethodNotAllowedException.class,
|
||||
() -> ResourceCompartmentUtil.getPatientCompartmentIdentity(myResource, myFhirContext, mySearchParamExtractor));
|
||||
|
||||
assertEquals(Msg.code(2475) + "Patient resource IDs must be client-assigned in patient compartment mode", thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenPatientResource_whichHasId_returnsId() {
|
||||
myCompartmentSearchParams = getMockSearchParams(true);
|
||||
when(myFhirContext.getResourceDefinition(myResource)).thenReturn(myRuntimeResourceDefinition);
|
||||
when(myRuntimeResourceDefinition.getSearchParams()).thenReturn(myCompartmentSearchParams);
|
||||
when(myRuntimeResourceDefinition.getName()).thenReturn("Patient");
|
||||
when(myResource.getIdElement().getIdPart()).thenReturn("Abc");
|
||||
|
||||
Optional<String> result = ResourceCompartmentUtil.getPatientCompartmentIdentity(myResource, myFhirContext, mySearchParamExtractor);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("Abc", result.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoPatientResource_returnsPatientCompartment() {
|
||||
// getResourceCompartment is tested independently, so here it is just (static) mocked
|
||||
|
||||
myCompartmentSearchParams = getMockSearchParams(true);
|
||||
when(myFhirContext.getResourceDefinition(myResource)).thenReturn(myRuntimeResourceDefinition);
|
||||
when(myRuntimeResourceDefinition.getSearchParams()).thenReturn(myCompartmentSearchParams);
|
||||
when(myRuntimeResourceDefinition.getName()).thenReturn("Observation");
|
||||
when(mySearchParamExtractor.getPathValueExtractor(myResource, "Observation.subject"))
|
||||
.thenReturn(() -> List.of(new Reference("Patient/P01")));
|
||||
|
||||
// try (MockedStatic<ResourceCompartmentUtil> mockedUtil = Mockito.mockStatic(ResourceCompartmentUtil.class)) {
|
||||
// mockedUtil.when(() -> ResourceCompartmentUtil.getResourceCompartment(
|
||||
// myResource, myCompartmentSearchParams, mySearchParamExtractor)).thenReturn(Optional.of("P01"));
|
||||
|
||||
// execute
|
||||
Optional<String> result = ResourceCompartmentUtil.getPatientCompartmentIdentity(myResource, myFhirContext, mySearchParamExtractor);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("P01", result.get());
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private List<RuntimeSearchParam> getMockSearchParams(boolean providePatientCompartment) {
|
||||
return List.of(
|
||||
getMockSearchParam("subject", "Observation.subject", providePatientCompartment),
|
||||
getMockSearchParam("performer", "Observation.performer", providePatientCompartment));
|
||||
}
|
||||
|
||||
private RuntimeSearchParam getMockSearchParam(String theName, String thePath, boolean providePatientCompartment) {
|
||||
RuntimeSearchParam rsp = mock(RuntimeSearchParam.class);
|
||||
lenient().when(rsp.getParamType()).thenReturn(RestSearchParameterTypeEnum.REFERENCE);
|
||||
lenient().when(rsp.getProvidesMembershipInCompartments()).thenReturn(getCompartmentsForParam(theName, providePatientCompartment));
|
||||
lenient().when(rsp.getName()).thenReturn(theName);
|
||||
lenient().when(rsp.getPath()).thenReturn(thePath);
|
||||
return rsp;
|
||||
}
|
||||
|
||||
private Set<String> getCompartmentsForParam(String theName, boolean theProvidePatientCompartment) {
|
||||
if (theProvidePatientCompartment) {
|
||||
return switch (theName) {
|
||||
case "subject" -> Set.of("Device", "Patient");
|
||||
case "performer" -> Set.of("Practitioner", "Patient", "RelatedPerson");
|
||||
default -> Collections.emptySet();
|
||||
};
|
||||
} else {
|
||||
return switch (theName) {
|
||||
case "subject" -> Set.of("Device");
|
||||
case "performer" -> Set.of("Practitioner", "RelatedPerson");
|
||||
default -> Collections.emptySet();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue