From 1badb96db833f2aac5b3374f21eba6afeb511a5b Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Mon, 15 Jun 2020 17:54:11 -0400 Subject: [PATCH] Add property for multitenancy --- .../jpa/starter/FhirServerConfigCommon.java | 9 +- .../uhn/fhir/jpa/starter/HapiProperties.java | 6 + .../fhir/jpa/starter/JpaRestfulServer.java | 10 ++ src/main/resources/hapi.properties | 5 + .../jpa/starter/MultitenantServerR4IT.java | 140 ++++++++++++++++++ 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigCommon.java b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigCommon.java index 74e10d3..344da8d 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigCommon.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/FhirServerConfigCommon.java @@ -135,7 +135,14 @@ public class FhirServerConfigCommon { @Bean public PartitionSettings partitionSettings() { - return new PartitionSettings(); + PartitionSettings retVal = new PartitionSettings(); + + // Partitioning + if (HapiProperties.getPartitioningMultitenancyEnabled()) { + retVal.setPartitioningEnabled(true); + } + + return retVal; } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java index 7f7e741..6639ef5 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java @@ -73,6 +73,8 @@ public class HapiProperties { static final String BULK_EXPORT_ENABLED = "bulk.export.enabled"; static final String EXPIRE_SEARCH_RESULTS_AFTER_MINS = "retain_cached_searches_mins"; static final String MAX_BINARY_SIZE = "max_binary_size"; + static final String PARTITIONING_MULTITENANCY_ENABLED = "partitioning.multitenancy.enabled"; + private static Properties ourProperties; public static boolean isElasticSearchEnabled() { @@ -489,5 +491,9 @@ public class HapiProperties { public static boolean isFhirPathFilterInterceptorEnabled() { return HapiProperties.getBooleanProperty("fhirpath_interceptor.enabled", false); } + + public static boolean getPartitioningMultitenancyEnabled() { + return HapiProperties.getBooleanProperty(PARTITIONING_MULTITENANCY_ENABLED, false); + } } diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java index d967a70..b239233 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.api.rp.ResourceProviderFactory; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; +import ca.uhn.fhir.jpa.partition.PartitionManagementProvider; import ca.uhn.fhir.jpa.provider.GraphQLProvider; import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; @@ -35,6 +36,8 @@ import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor; +import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; +import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.ResultSeverityEnum; import org.hl7.fhir.dstu3.model.Bundle; @@ -318,6 +321,13 @@ public class JpaRestfulServer extends RestfulServer { registerProvider(appCtx.getBean(BulkDataExportProvider.class)); } + // Partitioning + if (HapiProperties.getPartitioningMultitenancyEnabled()) { + registerInterceptor(new RequestTenantPartitionInterceptor()); + setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); + registerProviders(appCtx.getBean(PartitionManagementProvider.class)); + } + } } diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties index 44736f5..dc656b9 100644 --- a/src/main/resources/hapi.properties +++ b/src/main/resources/hapi.properties @@ -149,3 +149,8 @@ email.password= # Enable Websocket Subscription Channel subscription.websocket.enabled=false + +################################################### +# Partitioning And Multitenancy +################################################### +partitioning.multitenancy.enabled=false diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java new file mode 100644 index 0000000..23fb19a --- /dev/null +++ b/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java @@ -0,0 +1,140 @@ +package ca.uhn.fhir.jpa.starter; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.util.ProviderConstants; +import ca.uhn.fhir.rest.api.CacheControlDirective; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.client.interceptor.UrlTenantSelectionInterceptor; +import ca.uhn.fhir.test.utilities.JettyUtil; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.webapp.WebAppContext; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import java.nio.file.Paths; + +import static org.junit.Assert.assertEquals; + +public class MultitenantServerR4IT { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MultitenantServerR4IT.class); + private static IGenericClient ourClient; + private static FhirContext ourCtx; + private static int ourPort; + private static Server ourServer; + private static UrlTenantSelectionInterceptor ourClientTenantInterceptor; + + static { + HapiProperties.forceReload(); + HapiProperties.setProperty(HapiProperties.DATASOURCE_URL, "jdbc:h2:mem:dbr4-mt"); + HapiProperties.setProperty(HapiProperties.FHIR_VERSION, "R4"); + HapiProperties.setProperty(HapiProperties.SUBSCRIPTION_WEBSOCKET_ENABLED, "true"); + HapiProperties.setProperty(HapiProperties.PARTITIONING_MULTITENANCY_ENABLED, "true"); + ourCtx = FhirContext.forR4(); + } + + @Test + public void testCreateAndReadInTenantA() { + ourLog.info("Base URL is: " + HapiProperties.getServerAddress()); + + // Create tenant A + ourClientTenantInterceptor.setTenantId("DEFAULT"); + ourClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) + .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(1)) + .andParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("TENANT-A")) + .execute(); + + + ourClientTenantInterceptor.setTenantId("TENANT-A"); + Patient pt = new Patient(); + pt.addName().setFamily("Family A"); + ourClient.create().resource(pt).execute().getId(); + + Bundle searchResult = ourClient.search().forResource(Patient.class).returnBundle(Bundle.class).cacheControl(new CacheControlDirective().setNoCache(true)).execute(); + assertEquals(1, searchResult.getEntry().size()); + Patient pt2 = (Patient) searchResult.getEntry().get(0).getResource(); + assertEquals("Family A", pt2.getName().get(0).getFamily()); + } + + @Test + @Ignore + public void testCreateAndReadInTenantB() { + ourLog.info("Base URL is: " + HapiProperties.getServerAddress()); + + // Create tenant A + ourClientTenantInterceptor.setTenantId("DEFAULT"); + ourClient + .operation() + .onServer() + .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) + .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(1)) + .andParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("TENANT-B")) + .execute(); + + + ourClientTenantInterceptor.setTenantId("TENANT-B"); + Patient pt = new Patient(); + pt.addName().setFamily("Family B"); + ourClient.create().resource(pt).execute().getId(); + + Bundle searchResult = ourClient.search().forResource(Patient.class).returnBundle(Bundle.class).cacheControl(new CacheControlDirective().setNoCache(true)).execute(); + assertEquals(1, searchResult.getEntry().size()); + Patient pt2 = (Patient) searchResult.getEntry().get(0).getResource(); + assertEquals("Family B", pt2.getName().get(0).getFamily()); + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + String path = Paths.get("").toAbsolutePath().toString(); + + ourLog.info("Project base path is: {}", path); + + ourServer = new Server(0); + + WebAppContext webAppContext = new WebAppContext(); + webAppContext.setContextPath("/hapi-fhir-jpaserver"); + webAppContext.setDisplayName("HAPI FHIR"); + webAppContext.setDescriptor(path + "/src/main/webapp/WEB-INF/web.xml"); + webAppContext.setResourceBase(path + "/target/hapi-fhir-jpaserver-starter"); + webAppContext.setParentLoaderPriority(true); + + ourServer.setHandler(webAppContext); + ourServer.start(); + + ourPort = JettyUtil.getPortForStartedServer(ourServer); + + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000); + String ourServerBase = HapiProperties.getServerAddress(); + ourServerBase = "http://localhost:" + ourPort + "/hapi-fhir-jpaserver/fhir/"; + + ourClient = ourCtx.newRestfulGenericClient(ourServerBase); + ourClient.registerInterceptor(new LoggingInterceptor(true)); + + ourClientTenantInterceptor = new UrlTenantSelectionInterceptor(); + ourClient.registerInterceptor(ourClientTenantInterceptor); + } + + public static void main(String[] theArgs) throws Exception { + ourPort = 8080; + beforeClass(); + } +}