summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Schulz2026-11-03 12:02:08 +0100
committerPaul-Christian Volkmer2026-11-03 12:06:27 +0100
commit2df37cb52ee1d0e13d9a1a358609e900b86d29af (patch)
treefbcf14b552b1bc0aec24c0b8a0aff6e8d7f57f71
parenta07da577923bba319c76eb9bad568fde7a0f8e22 (diff)
feat: Vfps pseudonym service (#330)
Co-authored-by: Paul-Christian Volkmer <code@pcvolkmer.de>
-rw-r--r--README.md10
-rw-r--r--docs/configuration.md29
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt19
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt13
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt4
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/VfpsPseudonymGenerator.kt82
7 files changed, 164 insertions, 5 deletions
diff --git a/README.md b/README.md
index 4a00064..dd19331 100644
--- a/README.md
+++ b/README.md
@@ -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,
+)