diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index f07953eb7f7..3f349697a86 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -1476,7 +1476,7 @@ The Bulk Data web application writes the exported FHIR resources to an IBM Cloud "useServerTruststore": true }, "pageSize": 100, - "batchIdEncryptionKey": "example-password", + "batchIdEncodingKey": "example-password", "maxPartitions": 3, "maxInputs": 5 }, @@ -2356,7 +2356,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/bulkdata/core/file/resourceCountThreshold`|number|The number of resources at which to finish writing a given file. The actual number of resources written to a single file may be slightly above this number, dependent on the configured page size. Use `0` to indicate that there is no limit to the number of resources to be written to a single file.| |`fhirServer/bulkdata/core/azure/objectSizeThresholdMB`|number|The size, in megabytes, at which to finish writing a given object.| |`fhirServer/bulkdata/core/azure/objectResourceCountThreshold`|number|The number of resources at which to finish writing a given object. The actual number of resources written to a single object may be slightly above this number, dependent on the configured page size.| -|`fhirServer/bulkdata/core/batchIdEncryptionKey`|string|Encoding key for JavaBatch job id | +|`fhirServer/bulkdata/core/batchIdEncodingKey`|string|Encoding key for JavaBatch job id | |`fhirServer/bulkdata/core/pageSize`|number|The search page size for patient/group export and the legacy export, the default value is 1000 | |`fhirServer/bulkdata/core/maxPartitions`|number| The maximum number of simultaneous partitions that are processed per Export and Import | |`fhirServer/bulkdata/core/maxInputs`|number| The number of inputs allowed for $import | @@ -2535,6 +2535,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/bulkdata/core/cos/presignedExpiry`|86400| |`fhirServer/bulkdata/core/azure/objectSizeThresholdMB`|200| |`fhirServer/bulkdata/core/azure/objectResourceCountThreshold`|200000| +|`fhirServer/bulkdata/core/batchIdEncodingKey`|""| |`fhirServer/bulkdata/core/pageSize`|1000| |`fhirServer/bulkdata/core/maxPartitions`|5| |`fhirServer/bulkdata/core/maxInputs`|5| @@ -2717,7 +2718,7 @@ Cases where that behavior is not supported are marked below with an `N` in the ` |`fhirServer/bulkdata/core/cos/presignedExpiry`|Y|Y|Y| |`fhirServer/bulkdata/core/azure/objectSizeThresholdMB`|N|N|| |`fhirServer/bulkdata/core/azure/objectResourceCountThreshold`|N|N|| -|`fhirServer/bulkdata/core/batchIdEncryptionKey`|Y|N|Y| +|`fhirServer/bulkdata/core/batchIdEncodingKey`|Y|N|Y| |`fhirServer/bulkdata/core/pageSize`|Y|Y|Y| |`fhirServer/bulkdata/core/maxPartitions`|Y|Y|Y| |`fhirServer/bulkdata/core/maxInputs`|Y|Y|Y| diff --git a/docs/src/pages/guides/FHIRValidationGuide.md b/docs/src/pages/guides/FHIRValidationGuide.md index a3989837051..35e82a2e722 100644 --- a/docs/src/pages/guides/FHIRValidationGuide.md +++ b/docs/src/pages/guides/FHIRValidationGuide.md @@ -219,7 +219,7 @@ Below is the list of implementation guides that are packaged as part of the proj |--------------------|-----------------| |US Core|`3.1.1`, `4.0.0`, `5.0.1`| |CARIN Consumer Directed Payer Data Exchange (CARIN IG for BlueButton®)|`1.0.0`, `1.1.0`| -|minimal Common Oncology Data Elements (mCODE)|`1.0.0` +|minimal Common Oncology Data Elements (mCODE)|`1.0.0`| |Da Vinci Health Record Exchange (HREX)|`1.0.0`| |Da Vinci Payer Data Exchange (PDEX)|`1.0.0`, `2.0.0`| |Da Vinci Payer Data Exchange (PDEX) Plan Net|`1.0.0`, `1.1.0`| diff --git a/fhir-parent/pom.xml b/fhir-parent/pom.xml index 4208aa62b32..98a1c230608 100644 --- a/fhir-parent/pom.xml +++ b/fhir-parent/pom.xml @@ -726,6 +726,11 @@ icu4j 71.1 + + org.hashids + hashids + 1.0.3 + diff --git a/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json b/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json index 3fa58e72b70..27cb4e457b6 100644 --- a/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json +++ b/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json @@ -156,7 +156,7 @@ "resourceCountThreshold": 200000 }, "pageSize": 100, - "batchIdEncryptionKey": "change-password", + "batchIdEncodingKey": "change-password", "maxPartitions": 5, "maxInputs": 5, "maxChunkReadTime": "90000", diff --git a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json index 37813b9bffe..22a03e4a643 100644 --- a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json +++ b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json @@ -340,7 +340,7 @@ "resourceCountThreshold": 200000 }, "pageSize": 100, - "batchIdEncryptionKey": "change-password", + "batchIdEncodingKey": "change-password", "maxPartitions": 5, "maxInputs": 5 }, diff --git a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json index d009481f7a0..149c5c8d8b7 100644 --- a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json +++ b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json @@ -330,7 +330,7 @@ "resourceCountThreshold": 200000 }, "pageSize": 100, - "batchIdEncryptionKey": "change-password", + "batchIdEncodingKey": "change-password", "maxPartitions": 5, "maxInputs": 5 }, diff --git a/operation/fhir-operation-bulkdata/pom.xml b/operation/fhir-operation-bulkdata/pom.xml index 237c454f03f..1847a9ad529 100644 --- a/operation/fhir-operation-bulkdata/pom.xml +++ b/operation/fhir-operation-bulkdata/pom.xml @@ -19,6 +19,18 @@ fhir-server ${project.version} + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + org.hashids + hashids + jakarta.ws.rs jakarta.ws.rs-api @@ -34,14 +46,6 @@ jsonassert test - - org.apache.httpcomponents - httpclient - - - org.apache.httpcomponents - httpcore - org.apache.cxf cxf-rt-frontend-jaxrs diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java index 4db2e54e26c..9a7bab445a4 100644 --- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java +++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java @@ -39,7 +39,6 @@ import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; - import org.linuxforhealth.fhir.config.FHIRRequestContext; import org.linuxforhealth.fhir.exception.FHIROperationException; import org.linuxforhealth.fhir.model.generator.exception.FHIRGeneratorException; @@ -55,7 +54,6 @@ import org.linuxforhealth.fhir.operation.bulkdata.model.JobInstanceRequest; import org.linuxforhealth.fhir.operation.bulkdata.model.JobInstanceResponse; import org.linuxforhealth.fhir.operation.bulkdata.model.PollingLocationResponse; -import org.linuxforhealth.fhir.operation.bulkdata.model.transformer.JobIdEncodingTransformer; import org.linuxforhealth.fhir.operation.bulkdata.model.type.Input; import org.linuxforhealth.fhir.operation.bulkdata.model.type.JobParameter; import org.linuxforhealth.fhir.operation.bulkdata.model.type.JobType; @@ -63,6 +61,7 @@ import org.linuxforhealth.fhir.operation.bulkdata.model.type.StorageType; import org.linuxforhealth.fhir.operation.bulkdata.model.url.DownloadUrl; import org.linuxforhealth.fhir.operation.bulkdata.util.BulkDataExportUtil; +import org.linuxforhealth.fhir.operation.bulkdata.util.CommonUtil; /** * BulkData Client to connect to the other server. @@ -227,7 +226,7 @@ public String submitExport(Instant since, Set types, ExportType exportTy CloseableHttpResponse jobResponse = cli.execute(jobPost); int status = -1; - String jobId = "-1"; + int jobId = -1; try { status = jobResponse.getStatusLine().getStatusCode(); handleStandardResponseStatus(status); @@ -243,7 +242,7 @@ public String submitExport(Instant since, Set types, ExportType exportTy String responseString = new BasicResponseHandler().handleResponse(jobResponse); JobInstanceResponse response = JobInstanceResponse.Parser.parse(responseString); - jobId = Integer.toString(response.getInstanceId()); + jobId = response.getInstanceId(); } finally { jobPost.releaseConnection(); @@ -251,7 +250,7 @@ public String submitExport(Instant since, Set types, ExportType exportTy } cli.close(); - return baseUri + "/$bulkdata-status?job=" + JobIdEncodingTransformer.getInstance().encodeJobId(jobId); + return baseUri + "/$bulkdata-status?job=" + CommonUtil.encodeJobId(jobId); } /** @@ -663,7 +662,7 @@ public String submitImport(String inputFormat, String inputSource, List i CloseableHttpResponse jobResponse = cli.execute(jobPost); int status = -1; - String jobId = "-1"; + int jobId = -1; try { status = jobResponse.getStatusLine().getStatusCode(); handleStandardResponseStatus(status); @@ -682,7 +681,7 @@ public String submitImport(String inputFormat, String inputSource, List i JobInstanceResponse response = JobInstanceResponse.Parser.parse(responseString); - jobId = Integer.toString(response.getInstanceId()); + jobId = response.getInstanceId(); } finally { jobPost.releaseConnection(); @@ -690,7 +689,7 @@ public String submitImport(String inputFormat, String inputSource, List i } cli.close(); - return baseUri + "/$bulkdata-status?job=" + JobIdEncodingTransformer.getInstance().encodeJobId(jobId); + return baseUri + "/$bulkdata-status?job=" + CommonUtil.encodeJobId(jobId); } /** diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java index 5985f816a68..9dd7deb87ea 100644 --- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java +++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java @@ -215,13 +215,13 @@ public interface ConfigurationAdapter { int getCorePageSize(); /** - * get core batch id encryption key for the job id that is returned + * get core batch id encoding key for the job id that is returned * * @implNote System value. We want to minimize the conflict possiblity. * * @return */ - String getCoreBatchIdEncryptionKey(); + String getCoreBatchIdEncodingKey(); /** * get core max partitions @@ -557,10 +557,10 @@ default boolean isStorageProviderParquetEnabled(String provider) { boolean getStorageProviderUsesRequestAccessToken(String provider); /** - * allows multiple resources in a single file. - * - * @implNote this default is false. - * + * allows multiple resources in a single file. + * + * @implNote this default is false. + * * @param source * @return */ diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java index d2ac8e9247d..7c94679366a 100644 --- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java +++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java @@ -107,8 +107,8 @@ private static final int defaultCoreMaxParititions() { } @Override - public String getCoreBatchIdEncryptionKey() { - return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/core/batchIdEncryptionKey", null); + public String getCoreBatchIdEncodingKey() { + return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/core/batchIdEncodingKey", ""); } @Override @@ -403,7 +403,7 @@ public String getStorageProviderAuthTypeConnectionString(String provider) { @Override public String getProviderAzureServiceVersion(String provider) { - return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/storageProviders/" + provider + "/serviceVersion", null); + return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/storageProviders/" + provider + "/serviceVersion", null); } @Override diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformer.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformer.java deleted file mode 100644 index f28279f8df7..00000000000 --- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformer.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.linuxforhealth.fhir.operation.bulkdata.model.transformer; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.Base64; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; - -import org.linuxforhealth.fhir.config.FHIRRequestContext; -import org.linuxforhealth.fhir.operation.bulkdata.config.ConfigurationFactory; - -/** - * Manages the Job Id Encryption. - */ -public class JobIdEncodingTransformer { - - private static final String CLASSNAME = JobIdEncodingTransformer.class.getName(); - private static final Logger logger = Logger.getLogger(CLASSNAME); - - // Tenant Encryption key used for JavaBatch Job ID - private static ConcurrentMap KEY_MAP = new ConcurrentHashMap<>(); - - private static JobIdEncodingTransformer transformer = null; - - private JobIdEncodingTransformer() { - // No Operation - } - - /** - * get the instance - * - * @return - */ - public static JobIdEncodingTransformer getInstance() { - if (transformer == null) { - transformer = new JobIdEncodingTransformer(); - } - return transformer; - } - - /* - * gets the tenant specific encryption key. - * @return - */ - private SecretKeySpec getJobIdEncryptionKey() { - String encryptionKey = ConfigurationFactory.getInstance().getCoreBatchIdEncryptionKey(); - SecretKeySpec secretKey = null; - - if (encryptionKey != null && !encryptionKey.isEmpty()) { - try { - byte[] keyBytes = encryptionKey.getBytes("UTF-8"); - keyBytes = Arrays.copyOf(MessageDigest.getInstance("SHA-1").digest(keyBytes), 16); - secretKey = new SecretKeySpec(keyBytes, "AES"); - } catch (Exception e) { - logger.log(Level.WARNING, "Fail to generate encryption key from config!", e); - } - } - - if (secretKey == null) { - logger.warning("Failed to get encryption key, JavaBatch Job ids will not be encrypted!"); - } - return secretKey; - } - - /** - * encodes the job id - * - * @implNote note a Cipher is used here, however it is not used to encrypt sensitive information rather encode the jobId. - * - * @param jobId - * @return - */ - public String encodeJobId(String jobId) { - String tenantId = FHIRRequestContext.get().getTenantId(); - SecretKeySpec key = KEY_MAP.computeIfAbsent(tenantId, k -> getJobIdEncryptionKey()); - // Encrypt and UrlEncode the batch job id. - if (key == null) { - return jobId; - } else { - try { - // Use light weight encryption without salt to simplify both the encryption/decryption and also config. - Cipher cp = Cipher.getInstance("AES/ECB/PKCS5Padding"); - cp.init(Cipher.ENCRYPT_MODE, key); - - // Encrypt the job id, base64-encode it, and replace all `/` chars with the less problematic `_` char - String encodedJobId = Base64.getEncoder().withoutPadding().encodeToString(cp.doFinal(jobId.getBytes("UTF-8"))).replaceAll("/", "_"); - // The encrypted job id is used in the polling content location url directly, so urlencode here. - return java.net.URLEncoder.encode(encodedJobId, StandardCharsets.UTF_8.name()); - } catch (Exception e) { - return jobId; - } - } - } - - /** - * decodes the job id. - * - * @implNote note a Cipher is used here, however it is not used to encrypt sensitive information rather encode the - * jobId. - * - * @param encodedJobId - * @return - */ - public String decodeJobId(String encodedJobId) { - String tenantId = FHIRRequestContext.get().getTenantId(); - SecretKeySpec key = KEY_MAP.computeIfAbsent(tenantId, k -> getJobIdEncryptionKey()); - // Decrypt to get the batch job id. - if (key == null) { - return encodedJobId; - } else { - try { - // Use light weight encryption without salt to simplify both the encryption/decryption and also config. - Cipher cp = Cipher.getInstance("AES/ECB/PKCS5PADDING"); - cp.init(Cipher.DECRYPT_MODE, key); - // The encrypted job id has already been urldecoded by liberty runtime before reaching this function, - // so, we don't do urldecode here.) - return new String(cp.doFinal(Base64.getDecoder().decode(encodedJobId.replaceAll("_", "/"))), "UTF-8"); - } catch (Exception e) { - return encodedJobId; - } - } - } -} diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java index 9174319f427..4eb03da41bd 100644 --- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java +++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java @@ -18,8 +18,6 @@ import javax.ws.rs.core.MediaType; -import org.owasp.encoder.Encode; - import org.linuxforhealth.fhir.config.FHIRConfigHelper; import org.linuxforhealth.fhir.core.FHIRMediaType; import org.linuxforhealth.fhir.core.FHIRVersionParam; @@ -34,9 +32,9 @@ import org.linuxforhealth.fhir.operation.bulkdata.OperationConstants; import org.linuxforhealth.fhir.operation.bulkdata.OperationConstants.ExportType; import org.linuxforhealth.fhir.operation.bulkdata.model.PollingLocationResponse; -import org.linuxforhealth.fhir.operation.bulkdata.model.transformer.JobIdEncodingTransformer; import org.linuxforhealth.fhir.search.compartment.CompartmentHelper; import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext; +import org.owasp.encoder.Encode; /** * BulkData Util captures common methods @@ -319,15 +317,14 @@ public String checkAndValidateJob(Parameters parameters) throws FHIROperationExc if (parameters != null) { for (Parameters.Parameter parameter : parameters.getParameter()) { if (OperationConstants.PARAM_JOB.equals(parameter.getName().getValue()) - && parameter.getValue() != null && parameter.getValue().is(org.linuxforhealth.fhir.model.type.String.class)) { - String job = JobIdEncodingTransformer.getInstance().decodeJobId(parameter.getValue().as(org.linuxforhealth.fhir.model.type.String.class).getValue()); - - // The job is never going to be empty or null as STRING is never empty at this point. - if (job.contains("/") || job.contains("?")) { - throw new FHIROperationException("job passed is invalid and is not supported"); + && parameter.getValue() != null && parameter.getValue().is(FHIR_STRING)) { + try { + // Don't look at any other parameters. + return CommonUtil.decodeJobId(parameter.getValue().as(FHIR_STRING).getValue()); + } catch(IllegalArgumentException e) { + String msg = "invalid job id was passed"; + throw buildOperationException(msg, IssueType.INVALID, e); } - // Don't look at any other parameters. - return job; } } } diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java index b75ecb43c72..bc6c22b04c1 100644 --- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java +++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java @@ -1,11 +1,12 @@ /* - * (C) Copyright IBM Corp. 2019, 2021 + * (C) Copyright IBM Corp. 2019, 2022 * * SPDX-License-Identifier: Apache-2.0 */ package org.linuxforhealth.fhir.operation.bulkdata.util; +import org.hashids.Hashids; import org.linuxforhealth.fhir.config.FHIRRequestContext; import org.linuxforhealth.fhir.exception.FHIROperationException; import org.linuxforhealth.fhir.model.resource.OperationOutcome; @@ -21,6 +22,8 @@ * Common Util captures common methods */ public class CommonUtil { + // min length constant for the "Hashids" we use to obfuscate job ids + private static final int HASHID_MIN_LENGTH = 6; /** * Type of Operation Call @@ -115,14 +118,73 @@ public void verifyAllowedType(String storageType) throws FHIROperationException } } - public static FHIROperationException buildExceptionWithIssue(String msg, IssueType issueType) - throws FHIROperationException { + /** + * encode the job id as a short string for use in URLs + * + * @param jobId the numeric job id to encode + * @return + * @implNote The implementation uses the tenantId from the request context and the + * corresponding coreBatchIdEncodingKey from fhir-server-config as a seed for + * the encoding. + */ + public static String encodeJobId(long jobId) { + Hashids encoder = getHashids(); + return encoder.encode(jobId); + } + + /** + * decode the job id back into the string representation of its numeric job id + * + * @param encodedJobId a job id encoded via {@link #encodeJobId(long, String, String)} + * @return + * @throws IllegalArgumentException if the passed encodedJobId could not be decoded + * @implNote The implementation uses the tenantId from the request context and the + * corresponding coreBatchIdEncodingKey from fhir-server-config to decode. + */ + public static String decodeJobId(String encodedJobId) { + Hashids decoder = getHashids(); + long[] decodedArray = decoder.decode(encodedJobId); + if (decodedArray.length != 1) { + throw new IllegalArgumentException("expected an encodedJobId with a single part, but found " + decodedArray.length); + } + return Long.toString(decodedArray[0]); + } + + /** + * Get the tenantId from the request context and the corresponding coreBatchIdEncodingKey from + * fhir-server-config and use that as the seed for the returned Hashids + */ + private static Hashids getHashids() { + String configSecret = ConfigurationFactory.getInstance().getCoreBatchIdEncodingKey(); + String seed = FHIRRequestContext.get().getTenantId() + ":" + configSecret; + return new Hashids(seed, HASHID_MIN_LENGTH); + } + + /** + * Construct a FHIROperationException with the passed {@code msg} and + * a single OperationOutcome.Issue of type {@code issueType} + * + * @param msg + * @param issueType + * @return + * @throws FHIROperationException + */ + public static FHIROperationException buildExceptionWithIssue(String msg, IssueType issueType) { OperationOutcome.Issue ooi = FHIRUtil.buildOperationOutcomeIssue(msg, issueType); return new FHIROperationException(msg).withIssue(ooi); } - public static FHIROperationException buildExceptionWithIssue(String msg, Throwable cause, IssueType issueType) - throws FHIROperationException { + /** + * Construct a FHIROperationException with the passed {@code msg}, caused by (@code cause}, and + * a single OperationOutcome.Issue of type {@code issueType} + * + * @param msg + * @param cause + * @param issueType + * @return + * @throws FHIROperationException + */ + public static FHIROperationException buildExceptionWithIssue(String msg, Throwable cause, IssueType issueType) { OperationOutcome.Issue ooi = FHIRUtil.buildOperationOutcomeIssue(msg, issueType); return new FHIROperationException(msg, cause).withIssue(ooi); } diff --git a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformerTest.java b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformerTest.java deleted file mode 100644 index e7d763557f0..00000000000 --- a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformerTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021, 2022 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.linuxforhealth.fhir.operation.bulkdata.model.transformer; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNotNull; - -import java.lang.reflect.Method; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; - -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import org.linuxforhealth.fhir.config.FHIRConfiguration; -import org.linuxforhealth.fhir.config.FHIRRequestContext; -import org.linuxforhealth.fhir.exception.FHIRException; - -/** - * Tests the Job ID Encoding Transformer. - */ -public class JobIdEncodingTransformerTest { - - @BeforeClass - public void setup() { - FHIRConfiguration.setConfigHome("target/test-classes"); - } - - @BeforeMethod - public void startMethod(Method method) throws FHIRException { - - // Configure the request context for our search tests - FHIRRequestContext context = FHIRRequestContext.get(); - context.setTenantId("default"); - } - - @AfterMethod - public void clearThreadLocal() { - FHIRRequestContext.remove(); - } - - @Test - public void testTransformerRoundTrip() throws Exception { - // Using the legacy implementation for the configuration the encode/decode uses change-password - final JobIdEncodingTransformer transformer = JobIdEncodingTransformer.getInstance(); - - String jobId = transformer.decodeJobId("1"); - assertNotNull(jobId); - assertEquals(jobId, "1"); - - // This results in at least one case where the naive base64 encoding of the encoded jobId would - // 1. have a leading '/' which is prohibited by the S3 client; and - // 2. have consecutive '/' which can makes it harder to get - for (int i = 0; i < 2000; i++) { - jobId = String.valueOf(i); - - String encodedJobId = transformer.encodeJobId(jobId); - assertNotNull(encodedJobId); - assertFalse(encodedJobId.equals(jobId)); - assertFalse(encodedJobId.startsWith("/")); - assertFalse(encodedJobId.contains("//")); - - encodedJobId = URLDecoder.decode(encodedJobId, StandardCharsets.UTF_8.toString()); - assertNotNull(encodedJobId); - - String decodedJobId = transformer.decodeJobId(encodedJobId); - assertNotNull(decodedJobId); - assertEquals(decodedJobId, jobId); - } - } -} \ No newline at end of file diff --git a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataCommonUtilTest.java b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataCommonUtilTest.java new file mode 100644 index 00000000000..dc1a2c9b09a --- /dev/null +++ b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataCommonUtilTest.java @@ -0,0 +1,68 @@ +/* + * (C) Copyright IBM Corp. 2021, 2022 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.linuxforhealth.fhir.operation.bulkdata.util; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; + +import org.linuxforhealth.fhir.config.FHIRRequestContext; +import org.linuxforhealth.fhir.exception.FHIRException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Tests the Job ID Encoding Transformer. + */ +public class BulkDataCommonUtilTest { + @BeforeMethod + public void startMethod(Method method) throws FHIRException { + + // Configure the request context for our search tests + FHIRRequestContext context = FHIRRequestContext.get(); + context.setTenantId("default"); + } + + @AfterMethod + public void clearThreadLocal() { + FHIRRequestContext.remove(); + } + + @Test + public void testJobIdEncodingRoundTrip() throws Exception { + Set encodedIds = new HashSet<>(); + for (int i = 0; i < Math.pow(10, 5); i++) { + String jobId = String.valueOf(i); + + String encodedJobId = CommonUtil.encodeJobId(i); + assertNotNull(encodedJobId); + assertFalse(encodedJobId.equals(jobId)); + assertFalse(encodedJobId.contains("/"), "can't contain a slash"); + assertTrue(encodedIds.add(encodedJobId), "each encoded id must be unique"); + + // Ensure all the chars are URL-safe + assertEquals(encodedJobId, URLDecoder.decode(encodedJobId, StandardCharsets.UTF_8)); + + String decodedJobId = CommonUtil.decodeJobId(encodedJobId); + assertNotNull(decodedJobId); + assertEquals(decodedJobId, jobId); + } + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testJobIdDecodingInvalid() throws Exception { + CommonUtil.decodeJobId("bogus"); + } +} \ No newline at end of file diff --git a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java index a223ba5f9ed..b746e62e483 100644 --- a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java +++ b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java @@ -26,11 +26,6 @@ import javax.ws.rs.core.MediaType; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - import org.linuxforhealth.fhir.config.FHIRConfiguration; import org.linuxforhealth.fhir.config.FHIRRequestContext; import org.linuxforhealth.fhir.core.FHIRVersionParam; @@ -50,6 +45,10 @@ import org.linuxforhealth.fhir.operation.bulkdata.model.PollingLocationResponse; import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext; import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext.Type; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; /** * Test Export util @@ -504,15 +503,14 @@ public void testCheckAndValidateJobInvalidSlash() throws FHIROperationException @Test public void testCheckAndValidateJobValid() throws FHIROperationException { - List parameters = List.of(Parameter.builder().name(string("job")).value(string("1234q346")).build()); - Parameters.Builder builder = Parameters.builder(); - builder.id(UUID.randomUUID().toString()); - builder.parameter(parameters); - Parameters ps = builder.build(); + String encodedJobId = CommonUtil.encodeJobId(555L); + Parameters ps = Parameters.builder() + .parameter(Parameter.builder().name("job").value(encodedJobId).build()) + .build(); String result = util.checkAndValidateJob(ps); assertNotNull(result); - assertEquals(result, "1234q346"); + assertEquals(result, "555"); } @Test diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json index 4930e26e25b..c2d90dd4f8b 100644 --- a/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json +++ b/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json @@ -49,7 +49,7 @@ }, "pageSize": 100, "_comment": "max of 1000", - "batchIdEncryptionKey": "change-password", + "batchIdEncodingKey": "change-password", "maxPartitions": 3, "maxInputs": 5, "systemExportImpl": "none" diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json index 4927754695c..99fba683e38 100644 --- a/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json +++ b/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json @@ -21,7 +21,7 @@ }, "pageSize": 100, "_comment": "max of 1000", - "batchIdEncryptionKey": "change-password", + "batchIdEncodingKey": "change-password", "maxPartitions": 3, "maxInputs": 5, "systemExportImpl": "none" diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json index 4930e26e25b..c2d90dd4f8b 100644 --- a/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json +++ b/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json @@ -49,7 +49,7 @@ }, "pageSize": 100, "_comment": "max of 1000", - "batchIdEncryptionKey": "change-password", + "batchIdEncodingKey": "change-password", "maxPartitions": 3, "maxInputs": 5, "systemExportImpl": "none" diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json index a74d3df863d..2365a31d165 100644 --- a/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json +++ b/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json @@ -25,7 +25,7 @@ "resourceCountThreshold": 200000 }, "pageSize": 100, - "batchIdEncryptionKey": "change-password", + "batchIdEncodingKey": "change-password", "maxPartitions": 3, "maxInputs": 5, "maxChunkReadTime": "90000",