1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-04-19 17:26:51 +00:00

feat #18: initial support for authentication

This commit is contained in:
Paul-Christian Volkmer 2024-01-11 13:09:55 +01:00
parent 5579ad1453
commit 8a11e6e85b
12 changed files with 272 additions and 13 deletions

View File

@ -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,

View File

@ -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")

View File

@ -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>) {

View File

@ -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

View File

@ -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()
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -74,20 +74,25 @@ nav > ul {
nav > ul > li {
display: inline-block;
padding: 0 1rem;
}
nav > ul > li.login {
margin: 0 0 0 1em;
padding: 0 0 0 2em;
border-left: 1px solid var(--table-border);
}
nav > ul > li:first-of-type {
border-left: none;
}
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);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>