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:
12
README.md
12
README.md
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -23,4 +23,6 @@ public interface Generator {
|
||||
|
||||
String generate(String id);
|
||||
|
||||
String generateGenomDeTan(String id);
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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?,
|
||||
) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -35,6 +35,10 @@ class PseudonymizeService(
|
||||
}
|
||||
}
|
||||
|
||||
fun genomDeTan(patientId: PatientId): String {
|
||||
return generator.generateGenomDeTan(patientId.value)
|
||||
}
|
||||
|
||||
fun prefix(): String {
|
||||
return configProperties.prefix
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
Reference in New Issue
Block a user