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
|
Zusätzlich zur Patienten Identifier Pseudonymisierung müssen Vorgangsummern generiert werden, die
|
||||||
jede Übertragung eindeutig identifizieren aber gleichzeitig dem Patienten zugeordnet werden können.
|
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.
|
Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Originalwert mehrere
|
||||||
Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch *APP_PSEUDONYMIZE_GPAS_CCDN*).
|
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
|
### 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_TARGET`: gPas Domänenname für Patienten ID
|
||||||
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
|
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
|
||||||
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
|
* `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
|
### Anmeldung mit einem Passwort
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ dependencies {
|
|||||||
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
|
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
|
||||||
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
|
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
|
||||||
implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}")
|
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.mariadb.jdbc:mariadb-java-client")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
|
@ -48,7 +48,7 @@ class GpasPseudonymGeneratorTest {
|
|||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
val gPasConfigProperties = GPasConfigProperties(
|
val gPasConfigProperties = GPasConfigProperties(
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
||||||
"test",
|
"test", "test2",
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
@ -64,7 +64,13 @@ class GpasPseudonymGeneratorTest {
|
|||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890"))
|
withStatus(HttpStatus.OK).body(
|
||||||
|
getDummyResponseBody(
|
||||||
|
"1234",
|
||||||
|
"test",
|
||||||
|
"test1234ABCDEF567890"
|
||||||
|
)
|
||||||
|
)
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +96,10 @@ class GpasPseudonymGeneratorTest {
|
|||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.FOUND)
|
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)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,4 +23,6 @@ public interface Generator {
|
|||||||
|
|
||||||
String generate(String id);
|
String generate(String id);
|
||||||
|
|
||||||
|
String generateGenomDeTan(String id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -38,30 +38,41 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
|
|
||||||
private final static FhirContext r4Context = FhirContext.forR4();
|
private final static FhirContext r4Context = FhirContext.forR4();
|
||||||
private final String gPasUrl;
|
private final String gPasUrl;
|
||||||
private final String psnTargetDomain;
|
|
||||||
private final HttpHeaders httpHeader;
|
private final HttpHeaders httpHeader;
|
||||||
private final RetryTemplate retryTemplate;
|
private final RetryTemplate retryTemplate;
|
||||||
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
|
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
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.retryTemplate = retryTemplate;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
this.gPasUrl = gpasCfg.getUri();
|
this.gPasUrl = gpasCfg.getUri();
|
||||||
this.psnTargetDomain = gpasCfg.getTarget();
|
this.psnTargetDomain = gpasCfg.getTarget();
|
||||||
|
this.genomDeDomain = gpasCfg.getGenomDeDomain();
|
||||||
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
|
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
|
@Override
|
||||||
public String generate(String id) {
|
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 responseEntity = getGpasPseudonym(gPasRequestBody);
|
||||||
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
||||||
.parseResource(responseEntity.getBody());
|
.parseResource(responseEntity.getBody());
|
||||||
|
|
||||||
return unwrapPseudonym(gPasPseudonymResult);
|
return unwrapPseudonym(gPasPseudonymResult);
|
||||||
}
|
}
|
||||||
@ -75,9 +86,9 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final var identifier = (Identifier) parameters.get().getPart().stream()
|
final var identifier = (Identifier) parameters.get().getPart().stream()
|
||||||
.filter(a -> a.getName().equals("pseudonym"))
|
.filter(a -> a.getName().equals("pseudonym"))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseGet(ParametersParameterComponent::new).getValue();
|
.orElseGet(ParametersParameterComponent::new).getValue();
|
||||||
|
|
||||||
// pseudonym
|
// pseudonym
|
||||||
return sanitizeValue(identifier.getValue());
|
return sanitizeValue(identifier.getValue());
|
||||||
@ -106,8 +117,8 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
responseEntity = retryTemplate.execute(
|
responseEntity = retryTemplate.execute(
|
||||||
ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
|
ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
|
||||||
String.class));
|
String.class));
|
||||||
|
|
||||||
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||||
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
|
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
|
||||||
@ -119,16 +130,16 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
return responseEntity;
|
return responseEntity;
|
||||||
} catch (Exception unexpected) {
|
} catch (Exception unexpected) {
|
||||||
throw new PseudonymRequestFailed(
|
throw new PseudonymRequestFailed(
|
||||||
"API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
|
"API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getGpasRequestBody(String id) {
|
protected String getGpasRequestBody(String id, String targetDomain) {
|
||||||
var requestParameters = new Parameters();
|
var requestParameters = new Parameters();
|
||||||
requestParameters.addParameter().setName("target")
|
requestParameters.addParameter().setName("target")
|
||||||
.setValue(new StringType().setValue(psnTargetDomain));
|
.setValue(new StringType().setValue(targetDomain));
|
||||||
requestParameters.addParameter().setName("original")
|
requestParameters.addParameter().setName("original")
|
||||||
.setValue(new StringType().setValue(id));
|
.setValue(new StringType().setValue(id));
|
||||||
final IParser iParser = r4Context.newJsonParser();
|
final IParser iParser = r4Context.newJsonParser();
|
||||||
return iParser.encodeResourceToString(requestParameters);
|
return iParser.encodeResourceToString(requestParameters);
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ data class PseudonymizeConfigProperties(
|
|||||||
data class GPasConfigProperties(
|
data class GPasConfigProperties(
|
||||||
val uri: String?,
|
val uri: String?,
|
||||||
val target: String = "etl-processor",
|
val target: String = "etl-processor",
|
||||||
|
val genomDeDomain: String = "ccdn",
|
||||||
val username: String?,
|
val username: String?,
|
||||||
val password: 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.binary.Base32
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
|
import org.apache.commons.math3.random.RandomDataGenerator
|
||||||
|
|
||||||
|
|
||||||
class AnonymizingGenerator : Generator {
|
class AnonymizingGenerator : Generator {
|
||||||
@ -31,4 +32,9 @@ class AnonymizingGenerator : Generator {
|
|||||||
.lowercase()
|
.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 {
|
fun prefix(): String {
|
||||||
return configProperties.prefix
|
return configProperties.prefix
|
||||||
}
|
}
|
||||||
|
@ -317,3 +317,8 @@ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
|||||||
|
|
||||||
// TODO all other properties
|
// 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.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.output.*
|
import dev.dnpm.etl.processor.output.*
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
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.anonymizeContentWith
|
||||||
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
|
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 dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import org.apache.commons.codec.binary.Base32
|
import org.apache.commons.codec.binary.Base32
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
@ -68,6 +71,13 @@ class RequestProcessor(
|
|||||||
|
|
||||||
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
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 pseudonymizeWith pseudonymizeService
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
mtbFile anonymizeContentWith pseudonymizeService
|
||||||
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
||||||
@ -120,11 +130,14 @@ class RequestProcessor(
|
|||||||
|
|
||||||
val lastMtbFileRequestForPatient =
|
val lastMtbFileRequestForPatient =
|
||||||
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
||||||
val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
val isLastRequestDeletion =
|
||||||
|
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
||||||
|
|
||||||
return null != lastMtbFileRequestForPatient
|
return null != lastMtbFileRequestForPatient
|
||||||
&& !isLastRequestDeletion
|
&& !isLastRequestDeletion
|
||||||
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest)
|
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(
|
||||||
|
pseudonymizedMtbFileRequest
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId) {
|
fun processDeletion(patientId: PatientId) {
|
||||||
|
@ -207,7 +207,7 @@ class ExtensionsTest {
|
|||||||
inner class UsingDnpmV2Datamodel {
|
inner class UsingDnpmV2Datamodel {
|
||||||
|
|
||||||
val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json"
|
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 {
|
private fun fakeMtbFile(): Mtb {
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
||||||
|
@ -72,7 +72,7 @@ class PseudonymizeServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sanitizeFileName(@Mock generator: GpasPseudonymGenerator) {
|
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_")
|
assertThat(result).isEqualTo("l___a_bs_1_2_3_")
|
||||||
}
|
}
|
||||||
@ -90,4 +90,10 @@ class PseudonymizeServiceTest {
|
|||||||
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
|
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