diff options
7 files changed, 164 insertions, 5 deletions
@@ -13,6 +13,7 @@ So können zum Beispiel alle Lungenfälle an nNGM, sonstige Fällen an DNPM:DIP Zur Pseudonymgenerierung und Consentverwaltung werden die [Greifswalder Tools gPAS und gICS](https://www.ths-greifswald.de/) unterstützt. +Für Pseudonymgenerierung wird alternativ auch [Vfps](https://github.com/miracum/vfps) unterstützt. Diese Anwendung erlaubt das Entgegennehmen von Datensetzen im DNPM-Datenmodell 2.1 aus dem [Onkostar](https://www.it-choice.de/produkte/onkostar/)-Plugin @@ -27,6 +28,10 @@ https://github.com/pcvolkmer/mv64e-etl-processor/pkgs/container/mv64e-etl-proces ## Wichtige Änderungen +### 🔥 Wichtige Änderungen in Version 0.19 + +Ab Version 0.19 wird zur Pseudonymgenerierung alternativ Vfps unterstützt. + ### 🔥 Wichtige Änderungen in Version 0.18 Ab Version 0.18 kann diese Anwendung zur Datenausleitung über die nNGM-REST-API verwendet werden. @@ -61,8 +66,9 @@ Weitere Informationen zur Konfiguration der Generierung von Vorgangsnummern gibt ### Pseudonymisierung der Patienten-ID -Diese Anwendung nutzt die Pseudonymisierungsfunktion von gPAS. Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) -angegeben ist, wird diese verwendet. +Diese Anwendung nutzt die Pseudonymisierungsfunktion von gPAS oder Vfps. +Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) oder zu einer Vfps-Instanz angegeben ist, +wird diese verwendet. Ist diese nicht gesetzt, wird intern eine Anonymisierung der Patienten-ID vorgenommen. Weitere Informationen zur Konfiguration der Pseudonymisierung gibt es hier: diff --git a/docs/configuration.md b/docs/configuration.md index 50707b8..b487327 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -32,17 +32,22 @@ Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Origin Pseudonyme benötigt. Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch *APP_PSEUDONYMIZE_GPAS_CCDN*). +Bei Verwendung von Vfps wird ein Namespace benötigt, der mehrere Pseudonyme pro Originalwert unterstützt +(`APP_PSEUDONYMIZE_VFPS_GENOM_DE_TAN_NAMESPACE`). **WICHTIG:** Deaktivierte Pseudonymisierung ist nur für Tests nutzbar. Vorgangsummern sind zufällig und werden anschließend verworfen. ### Pseudonymisierung der Patienten-ID -Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) angegeben ist, wird diese verwendet. +Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) oder Vfps-Instanz angegeben ist, wird diese verwendet. Ist diese nicht gesetzt, wird intern eine Anonymisierung der Patienten-ID vorgenommen. * `APP_PSEUDONYMIZE_PREFIX`: Standortbezogenes Präfix - `UNKNOWN`, wenn nicht gesetzt -* `APP_PSEUDONYMIZE_GENERATOR`: `BUILDIN` oder `GPAS` - `BUILDIN`, wenn nicht gesetzt +* * `APP_PSEUDONYMIZE_GENERATOR`: + * `BUILDIN`: Interne SHA-256-/HMAC-Pseudonymisierung (Default) + * `GPAS`: Pseudonymisierung über gPAS + * `VFPS`: Pseudonymisierung über Vfps * `APP_PSEUDONYMIZE_HMAC_KEY`: Wenn in Verbindung mit `BUILDIN` gesetzt, wird anstelle `SHA256` nun `SHA256-HMAC` mit dem angegebenen Key verwendet. @@ -71,6 +76,26 @@ so ist nicht die URI der gPAS-Instanz anzugeben, sondern der SOAP-Endpoint: * `APP_PSEUDONYMIZE_GPAS_SOAP_ENDPOINT`: SOAP-Endpoint der gPAS-Instanz (e.g. http://127.0.0.1:9990/gpas/gpasService) +#### Pseudonymisierung mit Vfps + +Alternativ zu gPAS kann Vfps als Pseudonymisierungsdienst verwendet werden. + +Hierfür muss `APP_PSEUDONYMIZE_GENERATOR=VFPS` gesetzt sein und folgende Parameter: + +* `APP_PSEUDONYMIZE_VFPS_URI`: + Base-URL des Vfps-Dienstes +* `APP_PSEUDONYMIZE_VFPS_TOKEN`: + Bearer Token für die Authentifizierung +* `APP_PSEUDONYMIZE_VFPS_PATIENT_NAMESPACE`: + Namespace für Patientenpseudonyme +* `APP_PSEUDONYMIZE_VFPS_GENOM_DE_TAN_NAMESPACE`: + Multi-Pseudonym-Namespace für Vorgangsnummern + +Die Generierung von Patientenpseudonymen erfolgt über die Create-Operation, die Vorgangsnummerngenerierung über Append. + +Pseudonyme werden unverändert aus der Vfps-Antwort übernommen. +Es wird kein Standortpräfix hinzugefügt, da dies im Vfps-Namespace selbst konfiguriert werden kann. + #### Eingebaute Anonymisierung Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt index 99ef0a2..e1e9f0d 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt @@ -35,6 +35,7 @@ import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator import dev.dnpm.etl.processor.pseudonym.AnonymizingHmacGenerator import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator import dev.dnpm.etl.processor.pseudonym.GpasSoapPseudonymGenerator +import dev.dnpm.etl.processor.pseudonym.VfpsPseudonymGenerator import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.TokenService import dev.dnpm.etl.processor.services.RequestProcessor @@ -271,6 +272,24 @@ class AppConfigurationTest { } @Nested + @TestPropertySource( + properties = + [ + "app.pseudonymize.generator=vfps", + "app.pseudonymize.vfps.uri=http://localhost/", + "app.pseudonymize.vfps.patient-namespace=test", + ], + ) + inner class AppConfigurationPseudonymizeGeneratorVfpsTest( + private val context: ApplicationContext, + ) { + @Test + fun shouldUseConfiguredGenerator() { + assertThat(context.getBean<VfpsPseudonymGenerator>()).isNotNull + } + } + + @Nested @TestPropertySource(properties = ["app.security.enable-tokens=true"]) @MockitoBean( types = [InMemoryUserDetailsManager::class, PasswordEncoder::class, TokenRepository::class], diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt index f0766a4..5cec5eb 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -63,6 +63,18 @@ data class GPasConfigProperties( } } +@ConfigurationProperties(VfpsConfigProperties.NAME) +data class VfpsConfigProperties( + val uri: String? = null, + val patientNamespace: String = "etl-processor", + val genomDeTanNamespace: String = "ccdn", + val token: String? = null, +) { + companion object { + const val NAME = "app.pseudonymize.vfps" + } +} + @ConfigurationProperties(ConsentConfigProperties.NAME) data class ConsentConfigProperties( var service: ConsentService = ConsentService.NONE, @@ -177,6 +189,7 @@ data class UserProperties( enum class PseudonymGenerator { BUILDIN, GPAS, + VFPS, } enum class ConsentService { diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt index 54f4c6f..44fd719 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -68,6 +68,7 @@ import kotlin.time.toJavaDuration AppConfigProperties::class, PseudonymizeConfigProperties::class, GPasConfigProperties::class, + VfpsConfigProperties::class, ConsentConfigProperties::class, GIcsConfigProperties::class, ] @@ -257,6 +258,17 @@ class AppConfiguration { ) } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "VFPS") + @Bean + fun vfpsPseudonymGenerator( + configProperties: VfpsConfigProperties, + retryTemplate: RetryTemplate, + restTemplate: RestTemplate, + ): Generator { + logger.info("Selected 'VfpsPseudonym Generator'") + return VfpsPseudonymGenerator(configProperties, retryTemplate, restTemplate) + } + @Bean fun jdbcConfiguration(): AbstractJdbcConfiguration { return AppJdbcConfiguration() diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt index 9eec9f3..c74345d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt @@ -30,7 +30,9 @@ class PseudonymizeService( ) { fun patientPseudonym(patientId: PatientId): PatientPseudonym = when (generator) { - is GpasPseudonymGenerator -> PatientPseudonym(generator.generate(patientId.value)) + is GpasPseudonymGenerator, + is VfpsPseudonymGenerator, + -> PatientPseudonym(generator.generate(patientId.value)) else -> PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}") } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/VfpsPseudonymGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/VfpsPseudonymGenerator.kt new file mode 100644 index 0000000..0a616b5 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/VfpsPseudonymGenerator.kt @@ -0,0 +1,82 @@ +package dev.dnpm.etl.processor.pseudonym + +import dev.dnpm.etl.processor.config.VfpsConfigProperties +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.retry.support.RetryTemplate +import org.springframework.web.client.RestClientException +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.postForObject +import org.springframework.web.util.UriComponentsBuilder + +class VfpsPseudonymGenerator( + private val configProperties: VfpsConfigProperties, + private val retryTemplate: RetryTemplate, + private val restTemplate: RestTemplate, +) : Generator { + private val uri = + configProperties.uri?.takeIf { it.isNotBlank() } + ?: throw IllegalArgumentException("vFPS URI must be configured") + + private val headers = + HttpHeaders().apply { + contentType = MediaType.APPLICATION_JSON + configProperties.token + ?.takeIf { it.isNotBlank() } + ?.let { setBearerAuth(it) } + } + + override fun generate(id: String): String = + execute { + retryTemplate.execute<String, Exception> { + request(configProperties.patientNamespace, "pseudonyms", id) + } + } + + override fun generateGenomDeTan(id: String): String = + execute { + request(configProperties.genomDeTanNamespace, "pseudonyms:append", id) + } + + private fun request( + namespace: String, + operation: String, + id: String, + ): String { + val url = + UriComponentsBuilder + .fromUriString(uri) + .pathSegment("v1", "namespaces", namespace, operation) + .build() + .encode() + .toUri() + + val response = + restTemplate.postForObject<VfpsResponse>( + url, + HttpEntity(mapOf("originalValue" to id), headers), + ) + + return response + ?.pseudonym + ?.pseudonymValue + ?.takeIf { it.isNotBlank() } + ?: throw PseudonymRequestFailed("No pseudonym returned from vFPS") + } + + private fun execute(action: () -> String): String = + try { + action() + } catch (ex: RestClientException) { + throw PseudonymRequestFailed("vFPS request failed", ex) + } +} + +private data class VfpsResponse( + val pseudonym: VfpsPseudonym? = null, +) + +private data class VfpsPseudonym( + val pseudonymValue: String? = null, +) |
