diff options
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | build.gradle.kts | 2 | ||||
| -rw-r--r-- | src/main/kotlin/dev/dnpm/etl/processor/EtlProcessorApplication.kt | 3 | ||||
| -rw-r--r-- | src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt | 12 | ||||
| -rw-r--r-- | src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt | 98 | ||||
| -rw-r--r-- | src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt | 33 | ||||
| -rw-r--r-- | src/main/resources/application-dev.yml | 3 | ||||
| -rw-r--r-- | src/main/resources/static/style.css | 69 | ||||
| -rw-r--r-- | src/main/resources/templates/fragments.html | 12 | ||||
| -rw-r--r-- | src/main/resources/templates/index.html | 5 | ||||
| -rw-r--r-- | src/main/resources/templates/login.html | 23 | ||||
| -rw-r--r-- | src/main/resources/templates/report.html | 5 |
12 files changed, 271 insertions, 12 deletions
@@ -52,6 +52,24 @@ Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfiguri * `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort * `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`: Root Zertifikat für gPas, falls es dediziert hinzugefügt werden muss. +### Anmeldung mit einem Passwort + +Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass bestimmte Bereiche nur nach +einem erfolgreichen Login erreichbar sind. + +* `APP_SECURITY_ADMIN_USER`: Muss angegeben werden zur Aktivierung der Zugriffsbeschränkung. +* `APP_SECURITY_ADMIN_PASSWORD`: Das Passwort für den Administrator (Empfohlen). + +Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der Anwendung in den Logs +angezeigt. + +#### Auswirkungen auf den dargestellten Inhalt + +Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die anonymisierte oder +pseudonymisierte Patienten-ID einsehen. + +Wurde kein Administrator-Account konfiguriert, sind diese Inhalte generell nicht verfügbar. + ### Transformation von Werten In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht, diff --git a/build.gradle.kts b/build.gradle.kts index f679c4f..55b8346 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,6 +54,8 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.kafka:spring-kafka") implementation("org.flywaydb:flyway-mysql") diff --git a/src/main/kotlin/dev/dnpm/etl/processor/EtlProcessorApplication.kt b/src/main/kotlin/dev/dnpm/etl/processor/EtlProcessorApplication.kt index 5d28c97..4b9b307 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/EtlProcessorApplication.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/EtlProcessorApplication.kt @@ -20,9 +20,10 @@ package dev.dnpm.etl.processor import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.boot.runApplication -@SpringBootApplication +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) class EtlProcessorApplication fun main(args: Array<String>) { 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 6b85603..9c92869 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -1,7 +1,7 @@ /* * This file is part of ETL-Processor * - * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors + * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published @@ -76,6 +76,16 @@ data class KafkaTargetProperties( } } +@ConfigurationProperties(SecurityConfigProperties.NAME) +data class SecurityConfigProperties( + val adminUser: String?, + val adminPassword: String?, +) { + companion object { + const val NAME = "app.security" + } +} + enum class PseudonymGenerator { BUILDIN, GPAS diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt new file mode 100644 index 0000000..68eb629 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt @@ -0,0 +1,98 @@ +/* + * This file is part of ETL-Processor + * + * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package dev.dnpm.etl.processor.config + +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.crypto.factory.PasswordEncoderFactories +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.web.SecurityFilterChain +import java.util.* + + +@Configuration +@EnableConfigurationProperties( + value = [ + SecurityConfigProperties::class + ] +) +@ConditionalOnProperty(value = ["app.security.admin-user"]) +@EnableWebSecurity +class AppSecurityConfiguration( + private val securityConfigProperties: SecurityConfigProperties +) { + + private val logger = LoggerFactory.getLogger(AppSecurityConfiguration::class.java) + + @Bean + fun userDetailsService(passwordEncoder: PasswordEncoder): InMemoryUserDetailsManager { + val adminUser = if (securityConfigProperties.adminUser.isNullOrBlank()) { + logger.warn("Using random Admin User: admin") + "admin" + } else { + securityConfigProperties.adminUser + } + val adminPassword = if (securityConfigProperties.adminPassword.isNullOrBlank()) { + val random = UUID.randomUUID().toString() + logger.warn("Using random Admin Passwort: {}", random) + random + } else { + securityConfigProperties.adminPassword + } + + val user: UserDetails = User.withUsername(adminUser) + .password(passwordEncoder.encode(adminPassword)) + .roles("ADMIN") + .build() + + return InMemoryUserDetailsManager(user) + } + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize("/configs/**", hasRole("ADMIN")) + authorize(anyRequest, permitAll) + } + formLogin { + loginPage = "/login" + } + csrf { disable() } + } + return http.build() + } + + @Bean + fun passwordEncoder(): PasswordEncoder { + return PasswordEncoderFactories.createDelegatingPasswordEncoder() + } + +} + diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt new file mode 100644 index 0000000..02c98cf --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt @@ -0,0 +1,33 @@ +/* + * This file is part of ETL-Processor + * + * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package dev.dnpm.etl.processor.web + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class LoginController { + + @GetMapping(path = ["/login"]) + fun login(): String { + return "login" + } + +}
\ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index dabe84b..d538338 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -10,6 +10,9 @@ app: topic: test response-topic: test_response servers: localhost:9094 + #security: + # admin-user: admin + # admin-password: very-secret server: port: 8000 diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css index b2ba085..fee5d4c 100644 --- a/src/main/resources/static/style.css +++ b/src/main/resources/static/style.css @@ -74,20 +74,25 @@ nav > ul { nav > ul > li { display: inline-block; padding: 0 1rem; - border-left: 1px solid var(--table-border); } -nav > ul > li:first-of-type { - border-left: none; +nav > ul > li.login { + margin: 0 0 0 1em; + padding: 0 0 0 2em; + border-left: 1px solid var(--table-border); } nav li a { - color: #004a8f; + color: var(--bg-blue); text-transform: uppercase; text-decoration: none; font-weight: 700; } +nav li.login a { + color: var(--bg-red); +} + nav li a:hover { text-decoration: underline; } @@ -177,6 +182,39 @@ form.samplecode-input input:focus-visible { background: lightgreen; } +.login-form { + width: fit-content; + margin: 3em auto; + padding: 2em 5em; + + border: 1px solid var(--table-border); + border-radius: .5em; + background: white; +} + +.login-form form { + width: 20em; + margin: 0 auto; + display: grid; + grid-gap: .5em; + + border: none; + background: none; +} + +.login-form form * { + padding: 0.5em; + border: 1px solid var(--table-border); + border-radius: 3px; +} + +.login-form button { + margin: 1em 0; + background: var(--bg-blue); + color: white; + border: none; +} + .border { padding: 1.5em; border: 1px solid var(--table-border); @@ -200,6 +238,7 @@ table { } .border > table { + padding: 0; border: none; background: transparent; } @@ -272,6 +311,13 @@ td > small { text-align: center; } +td.patient-id { + width: 32em; + text-overflow: ellipsis; + overflow: hidden; + display: block; +} + td.bg-blue, th.bg-blue, td.bg-green, th.bg-green, td.bg-yellow, th.bg-yellow, @@ -459,4 +505,19 @@ input.inline:focus-visible { .connection-display .connection.available { background: var(--bg-green); +} + +.notification { + margin: 1em; + padding: .5em; + border-radius: 3px; + text-align: center; +} + +.notification.success { + color: var(--bg-green); +} + +.notification.error { + color: var(--bg-red); }
\ No newline at end of file diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html index 677e841..7a9af2f 100644 --- a/src/main/resources/templates/fragments.html +++ b/src/main/resources/templates/fragments.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html xmlns:th="http://www.thymeleaf.org"> +<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <link rel="stylesheet" th:href="@{/style.css}" /> @@ -14,7 +14,15 @@ <ul> <li><a th:href="@{/}">Übersicht</a></li> <li><a th:href="@{/statistics}">Statistiken</a></li> - <li><a th:href="@{/configs}">Konfiguration</a></li> + <li sec:authorize="hasRole('ADMIN')"> + <a th:href="@{/configs}">Konfiguration</a> + </li> + <li class="login" sec:authorize="not isAuthenticated()"> + <a th:href="@{/login}">Login</a> + </li> + <li class="login" sec:authorize="isAuthenticated()"> + <a th:href="@{/logout}">Abmelden</a> + </li> </ul> </nav> </div> diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index b34804b..b1c3142 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="de" xmlns:th="http://www.thymeleaf.org"> +<html lang="de" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>ETL-Prozessor</title> @@ -42,7 +42,8 @@ <a th:href="@{/report/{id}(id=${request.uuid})}">[[ ${request.uuid} ]]</a> </td> <td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td> - <td>[[ ${request.patientId} ]]</td> + <td class="patient-id" sec:authorize="authenticated">[[ ${request.patientId} ]]</td> + <td class="patient-id" sec:authorize="not authenticated">***</td> </tr> </tbody> </table> diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..018122d --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="de" xmlns:th="http://www.thymeleaf.org"> +<head> + <meta charset="UTF-8"> + <title>ETL-Prozessor</title> + <link rel="stylesheet" th:href="@{/style.css}" /> +</head> +<body> + <div th:replace="~{fragments.html :: nav}"></div> + <main> + <div class="login-form"> + <h2 class="centered">Anmelden</h2> + <div class="centered notification error" th:if="${param.error}">Anmeldung nicht erfolgreich</div> + <div class="centered notification success" th:if="${param.logout}">Sie haben sich abgemeldet</div> + <form method="post" th:action="@{/login}"> + <input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus=""> + <input type="password" id="password" name="password" class="form-control" placeholder="Password" required=""> + <button class="" type="submit">Anmelden</button> + </form> + </div> + </main> +</body> +</html>
\ No newline at end of file diff --git a/src/main/resources/templates/report.html b/src/main/resources/templates/report.html index 01accc4..6f89345 100644 --- a/src/main/resources/templates/report.html +++ b/src/main/resources/templates/report.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="de" xmlns:th="http://www.thymeleaf.org"> +<html lang="de" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>ETL-Prozessor</title> @@ -31,7 +31,8 @@ <td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td> <td>[[ ${request.uuid} ]]</td> <td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td> - <td>[[ ${request.patientId} ]]</td> + <td class="patient-id" sec:authorize="authenticated">[[ ${request.patientId} ]]</td> + <td class="patient-id" sec:authorize="not authenticated">***</td> </tr> </tbody> </table> |
