diff --git a/README.md b/README.md index ee59a32..9977802 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Falls in diesem Fall die Statusprüfung fehlschlägt, wird Status **abgelehnt** Ist die Prüfung über gIcs deaktiviert, wird der eingetragene Einwilligungsstatus der übermittelten MTB Datei geprüft. * `APP_CONSENT_GICS_ENABLED`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn nicht gesetzt. -* `APP_CONSENT_GICS_GICSBASEURI`: URI der gICS-Instanz (z.B. `http://localhost:8090/ttp-fhir/fhir/gics`) +* `APP_CONSENT_GICS_URI`: URI der gICS-Instanz (z.B. `http://localhost:8090/ttp-fhir/fhir/gics`) * `APP_CONSENT_GICS_USERNAME`: gIcs Basic-Auth Benutzername * `APP_CONSENT_GICS_PASSWORD`: gIcs Basic-Auth Passwort * `APP_CONSENT_GICS_PERSONIDENTIFIERSYSTEM`: Derzeit wird nur die PID unterstützt. wenn leer wird `https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID` angenommen @@ -423,4 +423,4 @@ Die Datei `application-dev.yml` enthält hierzu die Konfiguration für das Profi Beim Ausführen der Integrationstests wird eine Testdatenbank in einem Docker-Container gestartet. Siehe hier auch die Klasse `AbstractTestcontainerTest` unter `src/integrationTest`. -Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar. \ No newline at end of file +Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar. diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt index 9f3ae62..8e5d38e 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt @@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.web import dev.dnpm.etl.processor.config.AppConfiguration import dev.dnpm.etl.processor.config.AppSecurityConfiguration import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult +import dev.dnpm.etl.processor.monitoring.GIcsConnectionCheckService import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService import dev.dnpm.etl.processor.output.MtbFileSender @@ -89,7 +90,8 @@ abstract class MockSink : Sinks.Many RequestProcessor::class, TransformationService::class, GPasConnectionCheckService::class, - RestConnectionCheckService::class + RestConnectionCheckService::class, + GIcsConnectionCheckService::class ] ) class ConfigControllerTest { @@ -182,7 +184,13 @@ class ConfigControllerTest { @Test fun testShouldNotSaveTokenWithExstingName() { - whenever(tokenService.addToken(anyString())).thenReturn(Result.failure(RuntimeException("Testfailure"))) + whenever(tokenService.addToken(anyString())).thenReturn( + Result.failure( + RuntimeException( + "Testfailure" + ) + ) + ) mockMvc.post("/configs/tokens") { with(user("admin").roles("ADMIN")) @@ -303,7 +311,10 @@ class ConfigControllerTest { val idCaptor = argumentCaptor() val roleCaptor = argumentCaptor() - verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture()) + verify(userRoleService, times(1)).updateUserRole( + idCaptor.capture(), + roleCaptor.capture() + ) assertThat(idCaptor.firstValue).isEqualTo(42) assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN) @@ -341,23 +352,26 @@ class ConfigControllerTest { @BeforeEach fun setup( - applicationContext: WebApplicationContext, + applicationContext: WebApplicationContext ) { this.webClient = MockMvcWebTestClient .bindToApplicationContext(applicationContext).build() } @Test - fun testShouldRequestSSE() { - val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now()) + fun testShouldRequestGPasSSE() { + val expectedEvent = + ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now()) connectionCheckUpdateProducer.tryEmitNext(expectedEvent) connectionCheckUpdateProducer.emitComplete { _, _ -> true } - val result = webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM).exchange() - .expectStatus().isOk() - .expectHeader().contentType(TEXT_EVENT_STREAM) - .returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java) + val result = + webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(TEXT_EVENT_STREAM) + .returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java) StepVerifier.create(result.responseBody) .expectNext(expectedEvent) diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java index 5876c13..8bfe7f8 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckFileBased.java @@ -1,7 +1,16 @@ package dev.dnpm.etl.processor.consent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class ConsentCheckFileBased implements ICheckConsent{ + private static final Logger log = LoggerFactory.getLogger(ConsentCheckFileBased.class); + + public ConsentCheckFileBased() { + log.info("ConsentCheckFileBased initialized..."); + } + @Override public TtpConsentStatus getTtpConsentStatus(String personIdentifierValue) { return TtpConsentStatus.UNKNOWN_CHECK_FILE; diff --git a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java index 876e1da..11a0c87 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -49,11 +49,12 @@ public class GicsConsentService implements ICheckConsent { this.fhirContext = appFhirConfig.fhirContext(); httpHeader = buildHeader(gIcsConfigProperties.getUsername(), gIcsConfigProperties.getPassword()); + log.info("GicsConsentService initialized..."); } public String getGicsUri() { if (url == null) { - final String gIcsBaseUri = gIcsConfigProperties.getGIcsBaseUri(); + final String gIcsBaseUri = gIcsConfigProperties.getUri(); if (StringUtils.isBlank(gIcsBaseUri)) { throw new IllegalArgumentException( "gICS base URL is empty - should call gICS with false configuration."); 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 6b6fd82..311f149 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -62,7 +62,7 @@ data class GIcsConfigProperties( * Base URL to gICS System * */ - val gIcsBaseUri: String?, + val uri: String?, val username: String?, val password: String?, 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 2423a6c..dcd8469 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -23,10 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import dev.dnpm.etl.processor.consent.ConsentCheckFileBased import dev.dnpm.etl.processor.consent.ICheckConsent import dev.dnpm.etl.processor.consent.GicsConsentService -import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult -import dev.dnpm.etl.processor.monitoring.ConnectionCheckService -import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService -import dev.dnpm.etl.processor.monitoring.ReportService +import dev.dnpm.etl.processor.monitoring.* import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator import dev.dnpm.etl.processor.pseudonym.Generator import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator @@ -180,15 +177,9 @@ class AppConfiguration { return AppJdbcConfiguration() } - @Bean - @ConditionalOnMissingBean - fun constService(): ICheckConsent { - return ConsentCheckFileBased() - } - @Bean @ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true") - fun gicsAccessConsent( gIcsConfigProperties: GIcsConfigProperties, + fun gicsConsentService( gIcsConfigProperties: GIcsConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): ICheckConsent { return GicsConsentService( gIcsConfigProperties, @@ -197,5 +188,21 @@ class AppConfiguration { appFhirConfig ) } + + @ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true") + @Bean + fun gIcsConnectionCheckService( + restTemplate: RestTemplate, + gIcsConfigProperties: GIcsConfigProperties, + connectionCheckUpdateProducer: Sinks.Many + ): ConnectionCheckService { + return GIcsConnectionCheckService(restTemplate, gIcsConfigProperties, connectionCheckUpdateProducer) + } + + @Bean + @ConditionalOnMissingBean + fun constService(): ICheckConsent { + return ConsentCheckFileBased() + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt index b845e21..fe02b69 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt @@ -20,6 +20,7 @@ package dev.dnpm.etl.processor.monitoring +import dev.dnpm.etl.processor.config.GIcsConfigProperties import dev.dnpm.etl.processor.config.GPasConfigProperties import dev.dnpm.etl.processor.config.RestTargetProperties import jakarta.annotation.PostConstruct @@ -68,6 +69,12 @@ sealed class ConnectionCheckResult { override val timestamp: Instant, override val lastChange: Instant ) : ConnectionCheckResult() + + data class GIcsConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant + ) : ConnectionCheckResult() } class KafkaConnectionCheckService( @@ -207,4 +214,57 @@ class GPasConnectionCheckService( override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult { return this.result } +} + +class GIcsConnectionCheckService( + private val restTemplate: RestTemplate, + private val gIcsConfigProperties: GIcsConfigProperties, + @Qualifier("connectionCheckUpdateProducer") + private val connectionCheckUpdateProducer: Sinks.Many +) : ConnectionCheckService { + + private var result = ConnectionCheckResult.GIcsConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = try { + + val uri = UriComponentsBuilder.fromUriString( + gIcsConfigProperties.uri.toString()).path("/metadata").build().toUri() + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + if (!gIcsConfigProperties.username.isNullOrBlank() && !gIcsConfigProperties.password.isNullOrBlank()) { + headers.setBasicAuth(gIcsConfigProperties.username, gIcsConfigProperties.password) + } + + val available = restTemplate.exchange( + uri, + HttpMethod.GET, + HttpEntity(headers), + Void::class.java + ).statusCode == HttpStatus.OK + + ConnectionCheckResult.GIcsConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { result.lastChange } else { Instant.now() } + ) + } catch (_: Exception) { + ConnectionCheckResult.GIcsConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { result.lastChange } else { Instant.now() } + ) + } + connectionCheckUpdateProducer.emitNext( + result, + Sinks.EmitFailureHandler.FAIL_FAST + ) + } + + override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult { + return this.result + } } \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt index 25ec7cc..ea89e98 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt @@ -19,10 +19,7 @@ package dev.dnpm.etl.processor.web -import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult -import dev.dnpm.etl.processor.monitoring.ConnectionCheckService -import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService -import dev.dnpm.etl.processor.monitoring.OutputConnectionCheckService +import dev.dnpm.etl.processor.monitoring.* import dev.dnpm.etl.processor.output.MtbFileSender import dev.dnpm.etl.processor.pseudonym.Generator import dev.dnpm.etl.processor.security.Role @@ -61,11 +58,15 @@ class ConfigController( val gPasConnectionAvailable = connectionCheckServices.filterIsInstance().firstOrNull()?.connectionAvailable() + val gIcsConnectionAvailable = + connectionCheckServices.filterIsInstance().firstOrNull()?.connectionAvailable() + model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName) model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) + model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) model.addAttribute("tokensEnabled", tokenService != null) if (tokenService != null) { model.addAttribute("tokens", tokenService.findAll()) @@ -119,6 +120,24 @@ class ConfigController( return "configs/gPasConnectionAvailable" } + @GetMapping(params = ["gIcsConnectionAvailable"]) + fun gIcsConnectionAvailable(model: Model): String { + val gIcsConnectionAvailable = + connectionCheckServices.filterIsInstance().firstOrNull()?.connectionAvailable() + + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) + if (tokenService != null) { + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", listOf()) + } + + return "configs/gIcsConnectionAvailable" + } + @PostMapping(path = ["tokens"]) fun addToken(@ModelAttribute("name") name: String, model: Model): String { if (tokenService == null) { @@ -190,6 +209,7 @@ class ConfigController( is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check" is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check" is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check" + is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check" } ServerSentEvent.builder() diff --git a/src/main/resources/templates/configs.html b/src/main/resources/templates/configs.html index d94deb6..e0056ee 100644 --- a/src/main/resources/templates/configs.html +++ b/src/main/resources/templates/configs.html @@ -49,6 +49,11 @@ +
+
+
+
+
diff --git a/src/main/resources/templates/configs/gIcsConnectionAvailable.html b/src/main/resources/templates/configs/gIcsConnectionAvailable.html new file mode 100644 index 0000000..907a5a2 --- /dev/null +++ b/src/main/resources/templates/configs/gIcsConnectionAvailable.html @@ -0,0 +1,24 @@ + +

🟦 gICS nicht konfiguriert - Einwilligung wird über Dateiinhalt geprüft

+
+ +

Verbindung zu gICS

+
+ Stand: +  |  + Letzte Änderung: +
+
+ Die Verbindung ist aktuell + verfügbar. + nicht verfügbar. +
+
+ ETL-Processor + + gICS + ETL-Processor + + gICS +
+
\ No newline at end of file diff --git a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java index f3e3e91..f47c3a7 100644 --- a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java +++ b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java @@ -3,6 +3,7 @@ package dev.dnpm.etl.processor.consent; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + import com.fasterxml.jackson.databind.ObjectMapper; import dev.dnpm.etl.processor.config.AppConfiguration; import dev.dnpm.etl.processor.config.AppFhirConfig; @@ -23,19 +24,19 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.client.MockRestServiceServer; - -@ContextConfiguration(classes = {GicsConsentService.class, - AppConfiguration.class, ObjectMapper.class}) +@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class}) @TestPropertySource(properties = {"app.consent.gics.enabled=true", - "app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"}) + "app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"}) @RestClientTest public class GicsConsentServiceTest { public static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics"; @Autowired MockRestServiceServer mockRestServiceServer; + @Autowired GicsConsentService gicsConsentService; + @Autowired AppConfiguration appConfiguration; @@ -45,6 +46,7 @@ public class GicsConsentServiceTest { @BeforeEach public void setUp() { mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate()); + } @Test @@ -54,7 +56,8 @@ public class GicsConsentServiceTest { .setValue(new BooleanType().setValue(true))); mockRestServiceServer.expect( - requestTo("http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond( + requestTo("http://localhost:8090/ttp-fhir/fhir/gics" + + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond( withSuccess(appFhirConfig.fhirContext().newJsonParser() .encodeResourceToString(responseConsented), MediaType.APPLICATION_JSON)); @@ -63,7 +66,6 @@ public class GicsConsentServiceTest { assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENTED); } - @Test void consentRevoced() { final Parameters responseRevoced = new Parameters().addParameter( @@ -71,7 +73,8 @@ public class GicsConsentServiceTest { .setValue(new BooleanType().setValue(false))); mockRestServiceServer.expect( - requestTo("http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond( + requestTo("http://localhost:8090/ttp-fhir/fhir/gics" + + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond( withSuccess(appFhirConfig.fhirContext().newJsonParser() .encodeResourceToString(responseRevoced), MediaType.APPLICATION_JSON));