summaryrefslogtreecommitdiff
path: root/src
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 /src
parenta07da577923bba319c76eb9bad568fde7a0f8e22 (diff)
feat: Vfps pseudonym service (#330)
Co-authored-by: Paul-Christian Volkmer <code@pcvolkmer.de>
Diffstat (limited to 'src')
-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
5 files changed, 129 insertions, 1 deletions
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,
+)