Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/test/cd-agent/projects/fp-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
### FTS Project Configuration - FHIR Pseudonymizer Flow
##! Uses external FHIR Pseudonymizer instead of DeidentiFHIR

### Cohort Selection Configuration
cohortSelector:
trustCenterAgent:
server:
baseUrl: https://tc-agent:8080
auth:
basic:
user: cd-agent
password: Aj6cloJYsTpu+op+
ssl:
bundle: tca
domain: MII
patientIdentifierSystem: "http://fts.smith.care"
signerIdType: "Pseudonym"
policySystem: "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3"
policies:
- 2.16.840.1.113883.3.1937.777.24.5.3.2
- 2.16.840.1.113883.3.1937.777.24.5.3.3
- 2.16.840.1.113883.3.1937.777.24.5.3.6
- 2.16.840.1.113883.3.1937.777.24.5.3.7

### Data Selection Configuration
dataSelector:
everything:
fhirServer:
baseUrl: http://cd-hds:8080/fhir
pageSize: 500

### Deidentificator Configuration
##! Uses FHIR Pseudonymizer for identity pseudonymization via gPAS
deidentificator:
fhir-pseudonymizer:
serviceUrl:
baseUrl: http://fhir-pseudonymizer:8080/fhir
anonymizationConfig: /app/projects/fp-example/anonymization-config.yaml
trustCenterAgent:
server:
baseUrl: https://tc-agent:8080
auth:
basic:
user: cd-agent
password: Aj6cloJYsTpu+op+
ssl:
bundle: tca
domains:
pseudonym: MII
salt: MII
dateShift: MII
maxDateShift: P14D
dateShiftPreserve: NONE

### Bundle Sender Configuration
bundleSender:
researchDomainAgent:
server:
baseUrl: http://rd-agent:8080
auth:
oauth2:
registration: rd-agent
project: example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
fhirPathRules:
- path: "Encounter.period.start"
method: "dateshift"
- path: "Encounter.period.end"
method: "dateshift"
- path: "Observation.effectiveDateTime"
method: "dateshift"
- path: "Condition.onsetDateTime"
method: "dateshift"
- path: "Condition.recordedDate"
method: "dateshift"
1 change: 1 addition & 0 deletions .github/test/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ include:
- rd-agent/compose.yaml
- tc-agent/compose.yaml
- oauth2/compose.yaml
- fhir-pseudonymizer/compose.yaml

services:
# Clinical Domain
Expand Down
7 changes: 7 additions & 0 deletions .github/test/fhir-pseudonymizer/anonymization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fhirVersion: R4
fhirPathRules:
- path: "nodesByType('Identifier').where(system='http://fts.smith.care').value"
method: pseudonymize
domain: MII
- path: "Patient.name"
method: redact
21 changes: 21 additions & 0 deletions .github/test/fhir-pseudonymizer/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: fts-fhir-pseudonymizer

services:
fhir-pseudonymizer:
image: ghcr.io/miracum/fhir-pseudonymizer:v2.24.1
ports: [ ":8080" ]
networks: [ "clinical-domain", "agents" ]
environment:
PseudonymizationService: gPAS
gPAS__Url: https://tc-agent:8080/api/v2/cd/fp-gpas-proxy/
gPAS__Version: 2025.2.0
gPAS__Auth__Basic__Username: cd-agent
gPAS__Auth__Basic__Password: Aj6cloJYsTpu+op+
AnonymizationEngineConfigPath: /etc/anonymization.yaml
SSL_CERT_FILE: /etc/ssl/certs/ca.crt
volumes:
- ./anonymization.yaml:/etc/anonymization.yaml:ro
- ../ssl/ca.crt:/etc/ssl/certs/ca.crt:ro
depends_on:
tc-agent:
condition: service_healthy
15 changes: 15 additions & 0 deletions .github/test/results/fp-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"phase": "COMPLETED",
"sentBundles": 118,
"skippedBundles": 0,
"count": {
"total": 21,
"Patient": 100,
"Encounter": 760,
"Observation": 7855,
"Condition": 4675,
"DiagnosticReport": 499,
"Medication": 0,
"MedicationAdministration": 169
}
}
13 changes: 12 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,20 @@ jobs:
check-resources.sh example-with-fhir-consent.json "${LAST_UPDATED}"
check-pseudonymization.sh

- name: Clean RD-HDS for FHIR Pseudonymizer Test
run: make clean-rd-hds

- name: Run e2e for FHIR Pseudonymizer Flow
run: |
LAST_UPDATED=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
make transfer-all PROJECT=fp-example
make wait
make check-status RESULTS_FILE=fp-example.json
check-resources.sh fp-example.json "${LAST_UPDATED}"

- name: Collect Agent Logs
if: failure() || cancelled()
run: docker compose logs cd-agent tc-agent rd-agent
run: docker compose logs cd-agent tc-agent rd-agent fhir-pseudonymizer

- name: Collect MOSAIC Logs
if: failure() || cancelled()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package care.smith.fts.cda;

import static care.smith.fts.test.MockServerUtil.fhirResponse;
import static care.smith.fts.test.MockServerUtil.jsonResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import com.github.tomakehurst.wiremock.client.WireMock;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.utility.MountableFile;
import org.wiremock.integrations.testcontainers.WireMockContainer;

@Slf4j
public class FhirPseudonymizerE2E extends AbstractCohortSelectorE2E {

private final WireMockContainer fp =
new WireMockContainer("wiremock/wiremock:3.13.0")
.withCreateContainerCmdModifier(cmd -> cmd.withName("cda-e2e-fp-fp-example"))
.withNetwork(network)
.withNetworkAliases("fhir-pseudonymizer");

public FhirPseudonymizerE2E() {
super("fp-example.yaml");
cda.withCopyFileToContainer(
MountableFile.forClasspathResource("fp-example/anonymization-config.yaml"),
"/app/projects/fp-example/anonymization-config.yaml");
}

@BeforeEach
void setUpFp() {
fp.start();
configureFpMocks();
}

@AfterEach
void tearDownFp() {
if (fp.isRunning()) {
fp.stop();
}
}

private void configureFpMocks() {
var fpWireMock = new WireMock(fp.getHost(), fp.getPort());

// FP returns a bundle with 32-char Base64URL resource IDs (transport IDs)
var patient = new Patient();
patient.setId("AbCdEfGhIjKlMnOpQrStUvWxYz012345");
var responseBundle = new Bundle();
responseBundle.setType(Bundle.BundleType.COLLECTION);
responseBundle.addEntry().setResource(patient);

fpWireMock.register(
post(urlPathEqualTo("/fhir/$de-identify")).willReturn(fhirResponse(responseBundle)));
}

@Override
protected void setupSpecificTcaMocks() {
var tcaWireMock = new WireMock(tca.getHost(), tca.getPort());

var cohortGenerator =
createCohortGenerator("https://ths-greifswald.de/fhir/gics/identifiers/Pseudonym");
var tcaResponse =
new Bundle()
.setEntry(List.of(new BundleEntryComponent().setResource(cohortGenerator.generate())));

tcaWireMock.register(
post(urlPathMatching("/api/v2/cd/consented-patients.*"))
.withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON_VALUE))
.willReturn(fhirResponse(tcaResponse)));

tcaWireMock.register(
post(urlPathEqualTo("/api/v2/cd/fhir-pseudonymizer/transport-mapping"))
.willReturn(
jsonResponse(
"""
{"transferId": "AbCdEfGhIjKlMnOpQrStUvWxYz012345"}
""")));
}

@Test
void testFhirPseudonymizerTransfer() {
executeTransferTest("[]");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fhirPathRules:
- path: "Encounter.period.start"
method: "dateshift"
- path: "Encounter.period.end"
method: "dateshift"
39 changes: 39 additions & 0 deletions clinical-domain-agent/src/e2e/resources/projects/fp-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
cohortSelector:
trustCenterAgent:
server:
baseUrl: http://tc-agent:8080
domain: MII
patientIdentifierSystem: "http://fts.smith.care"
policySystem: "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3"
policies:
- 2.16.840.1.113883.3.1937.777.24.5.3.2
- 2.16.840.1.113883.3.1937.777.24.5.3.3
- 2.16.840.1.113883.3.1937.777.24.5.3.6
- 2.16.840.1.113883.3.1937.777.24.5.3.7

dataSelector:
everything:
fhirServer:
baseUrl: http://cd-hds:8080/fhir
pageSize: 500

deidentificator:
fhir-pseudonymizer:
serviceUrl:
baseUrl: http://fhir-pseudonymizer:8080/fhir
anonymizationConfig: /app/projects/fp-example/anonymization-config.yaml
trustCenterAgent:
server:
baseUrl: http://tc-agent:8080
domains:
pseudonym: MII
salt: MII
dateShift: MII
maxDateShift: P14D
dateShiftPreserve: NONE

bundleSender:
researchDomainAgent:
server:
baseUrl: http://rd-agent:8080
project: example
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package care.smith.fts.cda.impl;

import care.smith.fts.api.DateShiftPreserve;
import care.smith.fts.util.HttpClientConfig;
import care.smith.fts.util.tca.TcaDomains;
import java.io.File;
import java.time.Duration;
import java.util.Optional;

public record FhirPseudonymizerConfig(
HttpClientConfig serviceUrl,
File anonymizationConfig,
TCAConfig trustCenterAgent,
Duration maxDateShift,
DateShiftPreserve dateShiftPreserve) {

public FhirPseudonymizerConfig(
HttpClientConfig serviceUrl,
File anonymizationConfig,
TCAConfig trustCenterAgent,
Duration maxDateShift,
DateShiftPreserve dateShiftPreserve) {
this.serviceUrl = serviceUrl;
this.anonymizationConfig = anonymizationConfig;
this.trustCenterAgent = trustCenterAgent;
this.maxDateShift = maxDateShift;
this.dateShiftPreserve = Optional.ofNullable(dateShiftPreserve).orElse(DateShiftPreserve.NONE);
}

public record TCAConfig(HttpClientConfig server, TcaDomains domains) {}
}
Loading
Loading