1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-17 21:02:54 +00:00

feat: Participation at genomeDE §64e will trigger creation of a transaction number

This commit is contained in:
Jakub Lidke
2025-07-17 15:44:45 +02:00
parent 07508679e6
commit b5b6203f3a
13 changed files with 2140 additions and 1467 deletions

View File

@ -28,8 +28,13 @@ Konfigurationsparameter
Zusätzlich zur Patienten Identifier Pseudonymisierung müssen Vorgangsummern generiert werden, die
jede Übertragung eindeutig identifizieren aber gleichzeitig dem Patienten zugeordnet werden können.
Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Originalwert mehrere Pseudonyme benötigt.
Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch *APP_PSEUDONYMIZE_GPAS_CCDN*).
Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Originalwert mehrere
Pseudonyme benötigt.
Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch
*APP_PSEUDONYMIZE_GPAS_CCDN*).
**WICHTIG:** Deaktivierte Pseudonymisierung ist nur für Tests nutzbar. Vorgangsummern sind zufällig
und werden anschließend verworfen.
### Datenübermittlung über HTTP/REST
@ -110,7 +115,8 @@ Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfiguri
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname für Patienten ID
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
* `APP_PSEUDONYMIZE_GPAS_CCDN`: gPas Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (Clinical data node)
* `APP_PSEUDONYMIZE_GPAS_GENOM_DE_DOMAIN`: gPas Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (
Clinical data node)
### Anmeldung mit einem Passwort

View File

@ -90,6 +90,7 @@ dependencies {
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}")
implementation ("org.apache.commons:commons-math3:3.6.1")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
runtimeOnly("org.postgresql:postgresql")

View File

@ -48,7 +48,7 @@ class GpasPseudonymGeneratorTest {
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
val gPasConfigProperties = GPasConfigProperties(
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
"test",
"test", "test2",
null,
null
)
@ -64,7 +64,13 @@ class GpasPseudonymGeneratorTest {
method(HttpMethod.POST)
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
}.andRespond {
withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890"))
withStatus(HttpStatus.OK).body(
getDummyResponseBody(
"1234",
"test",
"test1234ABCDEF567890"
)
)
.createResponse(it)
}
@ -90,7 +96,10 @@ class GpasPseudonymGeneratorTest {
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
}.andRespond {
withStatus(HttpStatus.FOUND)
.header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
.header(
HttpHeaders.LOCATION,
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate"
)
.createResponse(it)
}

View File

@ -23,4 +23,6 @@ public interface Generator {
String generate(String id);
String generateGenomDeTan(String id);
}

View File

@ -38,27 +38,38 @@ public class GpasPseudonymGenerator implements Generator {
private final static FhirContext r4Context = FhirContext.forR4();
private final String gPasUrl;
private final String psnTargetDomain;
private final HttpHeaders httpHeader;
private final RetryTemplate retryTemplate;
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
private final RestTemplate restTemplate;
private final @NotNull String genomDeDomain;
private final @NotNull String psnTargetDomain;
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) {
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
RestTemplate restTemplate) {
this.retryTemplate = retryTemplate;
this.restTemplate = restTemplate;
this.gPasUrl = gpasCfg.getUri();
this.psnTargetDomain = gpasCfg.getTarget();
this.genomDeDomain = gpasCfg.getGenomDeDomain();
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
log.debug(String.format("%s has been initialized", this.getClass().getName()));
log.debug("{} has been initialized", this.getClass().getName());
}
@Override
public String generate(String id) {
var gPasRequestBody = getGpasRequestBody(id);
return generate(id, psnTargetDomain);
}
@Override
public String generateGenomDeTan(String id) {
return generate(id, genomDeDomain);
}
protected String generate(String id, String targetDomain) {
var gPasRequestBody = getGpasRequestBody(id, targetDomain);
var responseEntity = getGpasPseudonym(gPasRequestBody);
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
.parseResource(responseEntity.getBody());
@ -123,10 +134,10 @@ public class GpasPseudonymGenerator implements Generator {
}
}
protected String getGpasRequestBody(String id) {
protected String getGpasRequestBody(String id, String targetDomain) {
var requestParameters = new Parameters();
requestParameters.addParameter().setName("target")
.setValue(new StringType().setValue(psnTargetDomain));
.setValue(new StringType().setValue(targetDomain));
requestParameters.addParameter().setName("original")
.setValue(new StringType().setValue(id));
final IParser iParser = r4Context.newJsonParser();

View File

@ -48,6 +48,7 @@ data class PseudonymizeConfigProperties(
data class GPasConfigProperties(
val uri: String?,
val target: String = "etl-processor",
val genomDeDomain: String = "ccdn",
val username: String?,
val password: String?,
) {

View File

@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.pseudonym
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.math3.random.RandomDataGenerator
class AnonymizingGenerator : Generator {
@ -31,4 +32,9 @@ class AnonymizingGenerator : Generator {
.lowercase()
}
override fun generateGenomDeTan(id: String?): String? {
val randomDataGenerator = RandomDataGenerator()
return randomDataGenerator.nextSecureHexString(64).lowercase()
}
}

View File

@ -35,6 +35,10 @@ class PseudonymizeService(
}
}
fun genomDeTan(patientId: PatientId): String {
return generator.generateGenomDeTan(patientId.value)
}
fun prefix(): String {
return configProperties.prefix
}

View File

@ -317,3 +317,8 @@ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
// TODO all other properties
}
infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService)
{
this.metadata.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id))
}

View File

@ -29,8 +29,11 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
import dev.dnpm.etl.processor.output.*
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
import dev.dnpm.etl.processor.pseudonym.addGenomDeTan
import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
import dev.pcvolkmer.mv64e.mtb.Mtb
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
@ -68,6 +71,13 @@ class RequestProcessor(
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
val pid = PatientId(mtbFile.patient.id)
val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p ->
p.purpose == ModelProjectConsentPurpose.SEQUENCING
&& p.type == ConsentProvision.PERMIT
} == true
if (isModelProjectConsented) {
mtbFile addGenomDeTan pseudonymizeService
}
mtbFile pseudonymizeWith pseudonymizeService
mtbFile anonymizeContentWith pseudonymizeService
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
@ -120,11 +130,14 @@ class RequestProcessor(
val lastMtbFileRequestForPatient =
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
val isLastRequestDeletion =
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
return null != lastMtbFileRequestForPatient
&& !isLastRequestDeletion
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest)
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(
pseudonymizedMtbFileRequest
)
}
fun processDeletion(patientId: PatientId) {

View File

@ -207,7 +207,7 @@ class ExtensionsTest {
inner class UsingDnpmV2Datamodel {
val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json"
val CLEAN_PATIENT_ID = "aca5a971-28be-4089-8128-0036a4fe6f1a"
val CLEAN_PATIENT_ID = "644bae7a-56f6-4ee8-b02f-c532e65af5b1"
private fun fakeMtbFile(): Mtb {
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream

View File

@ -72,7 +72,7 @@ class PseudonymizeServiceTest {
@Test
fun sanitizeFileName(@Mock generator: GpasPseudonymGenerator) {
val result= GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
val result = GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
assertThat(result).isEqualTo("l___a_bs_1_2_3_")
}
@ -90,4 +90,10 @@ class PseudonymizeServiceTest {
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
}
@Test
fun shouldReturnDifferentValues() {
val ag = AnonymizingGenerator()
val tan = ag.generateGenomDeTan("12345")
assertThat(tan).hasSize(64)
}
}

File diff suppressed because it is too large Load Diff