Restrict bulk export download to specific user (#5052)

* Work

* Bulk export partitioning fixes

* Changelog fixes

* cleanup

* Work on security

* Compile fixes

* Work

* Test fix

* Work

* Add changelog

* License header changes

* Test fix

* Test fix

* Fixes

* Version bump

* Test fix

* Fix accidental change
This commit is contained in:
James Agnew 2023-07-05 12:48:15 -04:00 committed by GitHub
parent 9a6288c363
commit a93d06c25f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 817 additions and 92 deletions

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
@ -12,7 +12,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -0,0 +1,68 @@
/*-
* #%L
* HAPI FHIR - Docs
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.hapi.fhir.docs.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.binary.BinarySecurityContextInterceptor;
import org.hl7.fhir.instance.model.api.IBaseBinary;
/**
* This class is mostly intended as an example implementation of the
* {@link BinarySecurityContextInterceptor} although it could be used if
* you wanted its specific rules.
*/
public class HeaderBasedBinarySecurityContextInterceptor extends BinarySecurityContextInterceptor {
/**
* Header name
*/
public static final String X_SECURITY_CONTEXT_ALLOWED_IDENTIFIER = "X-SecurityContext-Allowed-Identifier";
/**
* Constructor
*
* @param theFhirContext The FHIR context
*/
public HeaderBasedBinarySecurityContextInterceptor(FhirContext theFhirContext) {
super(theFhirContext);
}
/**
* This method should be overridden in order to determine whether the security
* context identifier is allowed for the user.
*
* @param theSecurityContextSystem The <code>Binary.securityContext.identifier.system</code> value
* @param theSecurityContextValue The <code>Binary.securityContext.identifier.value</code> value
* @param theRequestDetails The request details associated with this request
*/
@Override
protected boolean securityContextIdentifierAllowed(String theSecurityContextSystem, String theSecurityContextValue, RequestDetails theRequestDetails) {
// In our simple example, we will use an incoming header called X-SecurityContext-Allowed-Identifier
// to determine whether the security context is allowed. This is typically not what you
// would want, since this is trusting the client to tell us what they are allowed
// to see. You would typically verify an access token or user session with something
// external, but this is a simple demonstration.
String actualHeaderValue = theRequestDetails.getHeader(X_SECURITY_CONTEXT_ALLOWED_IDENTIFIER);
String expectedHeaderValue = theSecurityContextSystem + "|" + theSecurityContextValue;
return expectedHeaderValue.equals(actualHeaderValue);
}
}

View File

@ -0,0 +1,6 @@
---
type: add
issue: 5052
title: "A new interceptor called BinarySecurityInterceptor has been added. This interceptor can
be used to enforce access to Binary resources by using values in the Binary.securityContext
element."

View File

@ -109,6 +109,7 @@ page.security.consent_interceptor=Consent Interceptor
page.security.search_narrowing_interceptor=Search Narrowing Interceptor
page.security.cors=CORS
page.security.balp_interceptor=Basic Audit Log Pattern (BALP)
page.security.binary_security_interceptor=Binary Resource Security Interceptor
section.validation.title=Validation
page.validation.introduction=Introduction

View File

@ -182,6 +182,9 @@ HAPI FHIR provides an interceptor that can be used to automatically generate and
HAPI FHIR provides an interceptor that can be used to implement user- and system-level authorization rules that are aware of FHIR semantics. See [Authorization](/docs/security/authorization_interceptor.html) for more information.
# Security: Binary Resources
HAPI FHIR provides an interceptor that can be used to secure access to Binary resources by using the `Binary.securityContext` element. See [Binary Security Interceptor](/docs/security/binary_security_interceptor.html) for more information.
# Security: Consent

View File

@ -0,0 +1,17 @@
# Binary Security Interceptor
The Binary resource has an element called `Binary.securityContext` that can be used to declare a security context for a given resource instance.
The **BinarySecurityContextInterceptor** can be used to verify whether a calling user/client should have access to a Binary resource they are trying to access.
Note that this interceptor can currently only enforce identifier values found in `Binary.securityContext.identifier`. Reference values found in `Binary.securityContext.reference` are not examined by this interceptor at this time, although this may be added in the future.
This interceptor is intended to be subclassed. A simple example is shown below:
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/interceptor/HeaderBasedBinarySecurityContextInterceptor.java}}
```
## Combining with Bulk Export
The `setBinarySecurityContextIdentifierSystem(..)` and `setBinarySecurityContextIdentifierValue(..)` properties on the `BulkExportJobParameters` object can be used to automatically populate the security context on Binary resources created by Bulk Export jobs with values that can be verified by this interceptor. An interceptor on the `STORAGE_INITIATE_BULK_EXPORT` pointcut is the easiest way to set these properties when a new Bulk Export job is being kicked off.

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.api.model.BulkExportJobResults;
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.Batch2JobDefinitionConstants;
@ -54,6 +55,7 @@ import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
@ -61,6 +63,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TagsInlineTest.createSearchParameterForInlineSecurity;
@ -69,6 +72,7 @@ import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -86,6 +90,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
void afterEach() {
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
myStorageSettings.setTagStorageMode(new JpaStorageSettings().getTagStorageMode());
myStorageSettings.setResourceClientIdStrategy(new JpaStorageSettings().getResourceClientIdStrategy());
}
@BeforeEach
@ -732,6 +737,28 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
assertEquals(40, finalJobInstance.getCombinedRecordsProcessed());
}
@Test
public void testSystemBulkExport_ClientIdModeNone() {
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.NOT_ALLOWED);
List<String> expectedIds = new ArrayList<>();
for (int i = 0; i < 20; i++) {
expectedIds.add(createPatient(withActiveTrue()).getValue());
expectedIds.add(createObservation(withStatus("final")).getValue());
}
final BulkExportJobParameters options = new BulkExportJobParameters();
options.setResourceTypes(Set.of("Patient", "Observation"));
options.setFilters(Set.of("Patient?active=true", "Patient?active=false", "Observation?status=final"));
options.setExportStyle(BulkExportJobParameters.ExportStyle.SYSTEM);
options.setOutputFormat(Constants.CT_FHIR_NDJSON);
JobInstance finalJobInstance = verifyBulkExportResults(options, expectedIds, List.of());
assertEquals(40, finalJobInstance.getCombinedRecordsProcessed());
}
@Test
public void testSystemBulkExport_WithBulkExportInclusionInterceptor() {
@ -739,7 +766,7 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
@Hook(Pointcut.STORAGE_BULK_EXPORT_RESOURCE_INCLUSION)
public boolean include(IBaseResource theResource) {
if (((Patient)theResource).getGender() == Enumerations.AdministrativeGender.FEMALE) {
if (((Patient) theResource).getGender() == Enumerations.AdministrativeGender.FEMALE) {
return false;
}
return true;
@ -767,10 +794,38 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
assertEquals(10, finalJobInstance.getCombinedRecordsProcessed());
} finally {
myInterceptorRegistry.unregisterInterceptorsIf(t->t instanceof BoysOnlyInterceptor);
myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof BoysOnlyInterceptor);
}
}
@Test
public void testSystemBulkExport_WithSecurityContext() {
List<String> expectedIds = new ArrayList<>();
for (int i = 0; i < 20; i++) {
expectedIds.add(createPatient(withActiveTrue()).getValue());
expectedIds.add(createObservation(withStatus("final")).getValue());
}
final BulkExportJobParameters options = new BulkExportJobParameters();
options.setResourceTypes(Set.of("Patient", "Observation"));
options.setFilters(Set.of("Patient?active=true", "Patient?active=false", "Observation?status=final"));
options.setExportStyle(BulkExportJobParameters.ExportStyle.SYSTEM);
options.setOutputFormat(Constants.CT_FHIR_NDJSON);
options.setBinarySecurityContextIdentifierSystem("http://foo");
options.setBinarySecurityContextIdentifierValue("bar");
JobInstance finalJobInstance = verifyBulkExportResults(options, expectedIds, List.of());
BulkExportJobResults results = JsonUtil.deserialize(finalJobInstance.getReport(), BulkExportJobResults.class);
List<String> binaryIds = results.getResourceTypeToBinaryIds().values().stream().flatMap(Collection::stream).toList();
assertEquals(2, binaryIds.size());
for (String next : binaryIds) {
Binary binary = myBinaryDao.read(new IdType(next), new SystemRequestDetails());
assertEquals("http://foo", binary.getSecurityContext().getIdentifier().getSystem());
assertEquals("bar", binary.getSecurityContext().getIdentifier().getValue());
}
}
private JobInstance verifyBulkExportResults(BulkExportJobParameters theOptions, List<String> theContainedList, List<String> theExcludedList) {
Batch2JobStartResponse startResponse = startNewJob(theOptions);
@ -798,6 +853,9 @@ public class BulkDataExportTest extends BaseResourceProviderR4Test {
List<String> binaryIds = file.getValue();
for (var nextBinaryId : binaryIds) {
String nextBinaryIdPart = new IdType(nextBinaryId).getIdPart();
assertThat(nextBinaryIdPart, matchesPattern("[a-zA-Z0-9]{32}"));
Binary binary = myBinaryDao.read(new IdType(nextBinaryId));
assertEquals(Constants.CT_FHIR_NDJSON, binary.getContentType());

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -110,6 +110,10 @@ public class BulkExportJobParameters implements IModelJson {
*/
@JsonProperty("partitionId")
private RequestPartitionId myPartitionId;
@JsonProperty("binarySecurityContextIdentifierSystem")
private String myBinarySecurityContextIdentifierSystem;
@JsonProperty("binarySecurityContextIdentifierValue")
private String myBinarySecurityContextIdentifierValue;
public String getExportIdentifier() {
return myExportId;
@ -231,6 +235,38 @@ public class BulkExportJobParameters implements IModelJson {
this.myPartitionId = thePartitionId;
}
/**
* Sets a value to place in the generated Binary resource's
* Binary.securityContext.identifier
*/
public void setBinarySecurityContextIdentifierSystem(String theBinarySecurityContextIdentifierSystem) {
myBinarySecurityContextIdentifierSystem = theBinarySecurityContextIdentifierSystem;
}
/**
* Sets a value to place in the generated Binary resource's
* Binary.securityContext.identifier
*/
public String getBinarySecurityContextIdentifierSystem() {
return myBinarySecurityContextIdentifierSystem;
}
/**
* Sets a value to place in the generated Binary resource's
* Binary.securityContext.identifier
*/
public void setBinarySecurityContextIdentifierValue(String theBinarySecurityContextIdentifierValue) {
myBinarySecurityContextIdentifierValue = theBinarySecurityContextIdentifierValue;
}
/**
* Sets a value to place in the generated Binary resource's
* Binary.securityContext.identifier
*/
public String getBinarySecurityContextIdentifierValue() {
return myBinarySecurityContextIdentifierValue;
}
public enum ExportStyle {
PATIENT, GROUP, SYSTEM
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
public class AuthorizationConstants {
public static final int ORDER_CONSENT_INTERCEPTOR = 100;
public static final int ORDER_BINARY_SECURITY_INTERCEPTOR = 150;
public static final int ORDER_AUTH_INTERCEPTOR = 200;
public static final int ORDER_CONVERTER_INTERCEPTOR = 300;

View File

@ -0,0 +1,154 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.interceptor.binary;
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.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationConstants;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseResource;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This security interceptor checks any Binary resources that are being exposed to
* a user and can forbid the user from accessing them based on the security context
* found in <code>Binary.securityContext.identifier</code>.
* <p>
* This interceptor is intended to be subclassed. The default implementation if it
* is not subclassed will reject any access to a Binary resource unless the
* request is a system request (using {@link SystemRequestDetails} or the Binary
* resource has no value in <code>Binary.securityContext.identifier</code>.
* </p>
* <p>
* Override {@link #securityContextIdentifierAllowed(String, String, RequestDetails)} in order
* to allow the user to access specific context values.
* </p>
*
* @since 6.8.0
*/
@SuppressWarnings("unused")
@Interceptor(order = AuthorizationConstants.ORDER_BINARY_SECURITY_INTERCEPTOR)
public class BinarySecurityContextInterceptor {
private final FhirContext myFhirContext;
/**
* Constructor
*
* @param theFhirContext The FHIR context
*/
public BinarySecurityContextInterceptor(FhirContext theFhirContext) {
Validate.notNull(theFhirContext, "theFhirContext must not be null");
myFhirContext = theFhirContext;
}
/**
* Interceptor hook method. Do not call this method directly.
*/
@Hook(Pointcut.STORAGE_PRESHOW_RESOURCES)
public void preShowResources(IPreResourceShowDetails theShowDetails, RequestDetails theRequestDetails) {
for (IBaseResource nextResource : theShowDetails.getAllResources()) {
if (nextResource instanceof IBaseBinary) {
applyAccessControl((IBaseBinary) nextResource, theRequestDetails);
}
}
}
/**
* Interceptor hook method. Do not call this method directly.
*/
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
public void preShowResources(IBaseResource theOldValue, IBaseResource theNewValue, RequestDetails theRequestDetails) {
if (theOldValue instanceof IBaseBinary) {
applyAccessControl((IBaseBinary) theOldValue, theRequestDetails);
}
}
/**
* This method applies security to a given Binary resource. It is not typically
* overridden but you could override it if you wanted to completely replace the
* security logic in this interceptor.
*
* @param theBinary The Binary resource being checked
* @param theRequestDetails The request details associated with this request
*/
protected void applyAccessControl(IBaseBinary theBinary, RequestDetails theRequestDetails) {
FhirTerser terser = myFhirContext.newTerser();
String securityContextSystem = terser.getSinglePrimitiveValueOrNull(theBinary, "securityContext.identifier.system");
String securityContextValue = terser.getSinglePrimitiveValueOrNull(theBinary, "securityContext.identifier.value");
if (isNotBlank(securityContextSystem) || isNotBlank(securityContextValue)) {
applyAccessControl(theBinary, securityContextSystem, securityContextValue, theRequestDetails);
}
}
/**
* This method applies access controls to a Binary resource containing the
* given identifier system and value in the Binary.securityContext element.
*
* @param theBinary The binary resource
* @param theSecurityContextSystem The identifier system
* @param theSecurityContextValue The identifier value
* @param theRequestDetails The request details
*/
protected void applyAccessControl(IBaseBinary theBinary, String theSecurityContextSystem, String theSecurityContextValue, RequestDetails theRequestDetails) {
if (theRequestDetails instanceof SystemRequestDetails) {
return;
}
if (securityContextIdentifierAllowed(theSecurityContextSystem, theSecurityContextValue, theRequestDetails)) {
return;
}
handleForbidden(theBinary);
}
/**
* Handles a non-permitted operation. This method throws a {@link ForbiddenOperationException}
* but you could override it to change that behaviour.
*/
protected void handleForbidden(IBaseBinary theBinary) {
throw new ForbiddenOperationException(Msg.code(2369) + "Security context not permitted");
}
/**
* Determines whether the current user has access to the given security
* context identifier. This method is intended to be overridden, the default
* implementation simply always returns <code>false</code>.
*
* @param theSecurityContextSystem The <code>Binary.securityContext.identifier.system</code> value
* @param theSecurityContextValue The <code>Binary.securityContext.identifier.value</code> value
* @param theRequestDetails The request details associated with this request
* @return Returns <code>true</code> if the request should be permitted, and <code>false</code> otherwise
*/
protected boolean securityContextIdentifierAllowed(String theSecurityContextSystem, String theSecurityContextValue, RequestDetails theRequestDetails) {
return false;
}
}

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.method;
import ca.uhn.fhir.rest.api.Constants;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.method;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.method;
import ca.uhn.fhir.i18n.Msg;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.method;
import ca.uhn.fhir.model.api.Include;

View File

@ -1,3 +1,22 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.method;
import org.hl7.fhir.instance.model.api.IBaseResource;

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -21,7 +21,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
</dependency>
<dependency>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -25,6 +25,8 @@ import ca.uhn.fhir.batch2.api.JobExecutionFailedException;
import ca.uhn.fhir.batch2.api.RunOutcome;
import ca.uhn.fhir.batch2.api.StepExecutionDetails;
import ca.uhn.fhir.batch2.jobs.export.models.BulkExportBinaryFileId;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.util.RandomTextUtils;
import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters;
import ca.uhn.fhir.batch2.jobs.export.models.ExpandedResourcesList;
import ca.uhn.fhir.context.FhirContext;
@ -36,8 +38,12 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.BinaryUtil;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
@ -50,6 +56,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.slf4j.LoggerFactory.getLogger;
public class WriteBinaryStep implements IJobStepWorker<BulkExportJobParameters, ExpandedResourcesList, BulkExportBinaryFileId> {
@ -107,13 +114,45 @@ public class WriteBinaryStep implements IJobStepWorker<BulkExportJobParameters,
}
SystemRequestDetails srd = new SystemRequestDetails();
RequestPartitionId partitionId = theStepExecutionDetails.getParameters().getPartitionId();
BulkExportJobParameters jobParameters = theStepExecutionDetails.getParameters();
RequestPartitionId partitionId = jobParameters.getPartitionId();
if (partitionId == null){
srd.setRequestPartitionId(RequestPartitionId.defaultPartition());
} else {
srd.setRequestPartitionId(partitionId);
}
DaoMethodOutcome outcome = binaryDao.create(binary,srd);
// Pick a unique ID and retry until we get one that isn't already used. This is just to
// avoid any possibility of people guessing the IDs of these Binaries and fishing for them.
while (true) {
// Use a random ID to make it harder to guess IDs - 32 characters of a-zA-Z0-9
// has 190 bts of entropy according to https://www.omnicalculator.com/other/password-entropy
String proposedId = RandomTextUtils.newSecureRandomAlphaNumericString(32);
binary.setId(proposedId);
// Make sure we don't accidentally reuse an ID. This should be impossible given the
// amount of entropy in the IDs but might as well be sure.
try {
IBaseBinary output = binaryDao.read(binary.getIdElement(), new SystemRequestDetails(), true);
if (output != null) {
continue;
}
} catch (ResourceNotFoundException e) {
// good
}
break;
}
if (myFhirContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
if (isNotBlank(jobParameters.getBinarySecurityContextIdentifierSystem()) || isNotBlank(jobParameters.getBinarySecurityContextIdentifierValue())) {
FhirTerser terser = myFhirContext.newTerser();
terser.setElement(binary, "securityContext.identifier.system", jobParameters.getBinarySecurityContextIdentifierSystem());
terser.setElement(binary, "securityContext.identifier.value", jobParameters.getBinarySecurityContextIdentifierValue());
}
}
DaoMethodOutcome outcome = binaryDao.update(binary,srd);
IIdType id = outcome.getId();
BulkExportBinaryFileId bulkExportBinaryFileId = new BulkExportBinaryFileId();

View File

@ -144,9 +144,10 @@ public class WriteBinaryStepTest {
// when
when(myDaoRegistry.getResourceDao(eq("Binary")))
.thenReturn(binaryDao);
when(binaryDao.create(any(IBaseBinary.class), any(RequestDetails.class)))
when(binaryDao.update(any(IBaseBinary.class), any(RequestDetails.class)))
.thenReturn(methodOutcome);
// test
RunOutcome outcome = myFinalStep.run(input, sink);
@ -156,7 +157,7 @@ public class WriteBinaryStepTest {
ArgumentCaptor<IBaseBinary> binaryCaptor = ArgumentCaptor.forClass(IBaseBinary.class);
ArgumentCaptor<SystemRequestDetails> binaryDaoCreateRequestDetailsCaptor = ArgumentCaptor.forClass(SystemRequestDetails.class);
verify(binaryDao)
.create(binaryCaptor.capture(), binaryDaoCreateRequestDetailsCaptor.capture());
.update(binaryCaptor.capture(), binaryDaoCreateRequestDetailsCaptor.capture());
String outputString = new String(binaryCaptor.getValue().getContent());
// post-pending a \n (as this is what the binary does)
String expected = String.join("\n", stringified) + "\n";

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.util.RandomTextUtils;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
@ -59,8 +60,6 @@ public abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
public static long DEFAULT_MAXIMUM_BINARY_SIZE = Long.MAX_VALUE - 1;
public static String BLOB_ID_PREFIX_APPLIED = "blob-id-prefix-applied";
private final SecureRandom myRandom;
private final String CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private final int ID_LENGTH = 100;
private long myMaximumBinarySize = DEFAULT_MAXIMUM_BINARY_SIZE;
private int myMinimumBinarySize;
@ -72,7 +71,7 @@ public abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
public BaseBinaryStorageSvcImpl() {
myRandom = new SecureRandom();
super();
}
@Override
@ -98,12 +97,7 @@ public abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc {
@Override
public String newBlobId() {
StringBuilder b = new StringBuilder();
for (int i = 0; i < ID_LENGTH; i++) {
int nextInt = Math.abs(myRandom.nextInt());
b.append(CHARS.charAt(nextInt % CHARS.length()));
}
return b.toString();
return RandomTextUtils.newSecureRandomAlphaNumericString(ID_LENGTH);
}
/**

View File

@ -0,0 +1,41 @@
/*-
* #%L
* HAPI FHIR Storage api
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.util;
import java.security.SecureRandom;
public class RandomTextUtils {
private static final SecureRandom ourRandom = new SecureRandom();
private static final String ALPHANUMERIC_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/**
* Creates a new string using randomly selected characters (using a secure random
* PRNG) containing letters (mixed case) and numbers.
*/
public static String newSecureRandomAlphaNumericString(int theLength) {
StringBuilder b = new StringBuilder();
for (int i = 0; i < theLength; i++) {
int nextInt = Math.abs(ourRandom.nextInt());
b.append(ALPHANUMERIC_CHARS.charAt(nextInt % ALPHANUMERIC_CHARS.length()));
}
return b.toString();
}
}

View File

@ -0,0 +1,211 @@
package ca.uhn.fhir.rest.server.interceptor.binary;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class BinarySecurityContextInterceptorTest {
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
@Order(0)
public static final RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.registerInterceptor(new HeaderBasedBinarySecurityContextInterceptor(ourCtx));
@RegisterExtension
@Order(1)
public static final HashMapResourceProviderExtension<Binary> ourBinaryProvider = new HashMapResourceProviderExtension<>(ourServer, Binary.class);
@RegisterExtension
@Order(1)
public static final HashMapResourceProviderExtension<Patient> ourPatientProvider = new HashMapResourceProviderExtension<>(ourServer, Patient.class);
@Test
void testRead_SecurityContextIdentifierPresent_RequestAllowedByInterceptor() {
storeBinaryWithSecurityContextIdentifier();
Binary actual = ourServer
.getFhirClient()
.read()
.resource(Binary.class)
.withId("A")
.withAdditionalHeader(HeaderBasedBinarySecurityContextInterceptor.X_SECURITY_CONTEXT_ALLOWED_IDENTIFIER, "http://foo|bar")
.execute();
assertEquals("A", actual.getIdElement().getIdPart());
assertEquals("http://foo", actual.getSecurityContext().getIdentifier().getSystem());
}
@Test
void testRead_SecurityContextIdentifierPresent_SystemRequestDetailsPermitted() {
storeBinaryWithSecurityContextIdentifier();
IBundleProvider results = ourBinaryProvider.searchAll(new SystemRequestDetails());
assertEquals(1, results.sizeOrThrowNpe());
}
@Test
void testRead_SecurityContextIdentifierPresent_RequestBlockedByInterceptor() {
storeBinaryWithSecurityContextIdentifier();
try {
ourServer
.getFhirClient()
.read()
.resource(Binary.class)
.withId("A")
.execute();
fail();
} catch (ForbiddenOperationException e) {
assertEquals("HTTP 403 Forbidden: HAPI-2369: Security context not permitted", e.getMessage());
}
}
@Test
void testRead_SecurityContextIdentifierNotPresent() {
storeBinaryWithoutSecurityContext();
Binary actual = ourServer
.getFhirClient()
.read()
.resource(Binary.class)
.withId("A")
.execute();
assertEquals("A", actual.getIdElement().getIdPart());
assertNull(actual.getSecurityContext().getIdentifier().getSystem());
}
@Test
void testRead_NonBinaryResource() {
Patient patient = new Patient();
patient.setId("Patient/A");
patient.setActive(true);
ourPatientProvider.store(patient);
Patient actual = ourServer
.getFhirClient()
.read()
.resource(Patient.class)
.withId("A")
.execute();
assertEquals("A", actual.getIdElement().getIdPart());
assertTrue(actual.getActive());
}
@Test
void testUpdate_SecurityContextIdentifierPresent_RequestAllowedByInterceptor() {
storeBinaryWithSecurityContextIdentifier();
Binary newBinary = new Binary();
newBinary.setId("Binary/A");
newBinary.setContentType("text/plain");
MethodOutcome outcome = ourServer
.getFhirClient()
.update()
.resource(newBinary)
.withAdditionalHeader(HeaderBasedBinarySecurityContextInterceptor.X_SECURITY_CONTEXT_ALLOWED_IDENTIFIER, "http://foo|bar")
.execute();
assertEquals(2L, outcome.getId().getVersionIdPartAsLong());
}
@Test
void testUpdate_SecurityContextIdentifierPresent_RequestBlockedByInterceptor() {
storeBinaryWithSecurityContextIdentifier();
try {
Binary newBinary = new Binary();
newBinary.setId("Binary/A");
newBinary.setContentType("text/plain");
ourServer
.getFhirClient()
.update()
.resource(newBinary)
.execute();
fail();
} catch (ForbiddenOperationException e) {
assertEquals("HTTP 403 Forbidden: HAPI-2369: Security context not permitted", e.getMessage());
}
}
private void storeBinaryWithoutSecurityContext() {
Binary binary = new Binary();
binary.setId("Binary/A");
binary.setContentType("text/plain");
ourBinaryProvider.store(binary);
}
/**
* This class also exists in hapi-fhir-docs - Make sure any changes here
* are reflected there too!
*/
private void storeBinaryWithSecurityContextIdentifier() {
Binary binary = new Binary();
binary.setId("Binary/A");
binary.getSecurityContext().getIdentifier().setSystem("http://foo");
binary.getSecurityContext().getIdentifier().setValue("bar");
ourBinaryProvider.store(binary);
}
/**
* This class also exists in hapi-fhir-docs - Update it there too if this changes!
*/
public static class HeaderBasedBinarySecurityContextInterceptor extends BinarySecurityContextInterceptor {
/**
* Header name
*/
public static final String X_SECURITY_CONTEXT_ALLOWED_IDENTIFIER = "X-SecurityContext-Allowed-Identifier";
/**
* Constructor
*
* @param theFhirContext The FHIR context
*/
public HeaderBasedBinarySecurityContextInterceptor(FhirContext theFhirContext) {
super(theFhirContext);
}
/**
* This method should be overridden in order to determine whether the security
* context identifier is allowed for the user.
*
* @param theSecurityContextSystem The <code>Binary.securityContext.identifier.system</code> value
* @param theSecurityContextValue The <code>Binary.securityContext.identifier.value</code> value
* @param theRequestDetails The request details associated with this request
*/
@Override
protected boolean securityContextIdentifierAllowed(String theSecurityContextSystem, String theSecurityContextValue, RequestDetails theRequestDetails) {
// In our simple example, we will use an incoming header called X-SecurityContext-Allowed-Identifier
// to determine whether the security context is allowed. This is typically not what you
// would want, since this is trusting the client to tell us what they are allowed
// to see. You would typically verify an access token or user session with something
// external, but this is a simple demonstration.
String actualHeaderValue = theRequestDetails.getHeader(X_SECURITY_CONTEXT_ALLOWED_IDENTIFIER);
String expectedHeaderValue = theSecurityContextSystem + "|" + theSecurityContextValue;
return expectedHeaderValue.equals(actualHeaderValue);
}
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<packaging>pom</packaging>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<name>HAPI-FHIR</name>
<description>An open-source implementation of the FHIR specification in Java.</description>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.7.11-SNAPSHOT</version>
<version>6.7.12-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>