1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-04-20 17:56:50 +00:00

Merge pull request #21 from CCC-MF/feat_18

feat #18: initial support for authentication
This commit is contained in:
Paul-Christian Volkmer 2024-01-11 13:32:37 +01:00 committed by GitHub
commit 21959c1698
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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_PASSWORD`: gPas Basic-Auth Passwort
* `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`: Root Zertifikat für gPas, falls es dediziert hinzugefügt werden muss. * `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 ### Transformation von Werten
In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht, 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-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jdbc") 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("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.kafka:spring-kafka")
implementation("org.flywaydb:flyway-mysql") implementation("org.flywaydb:flyway-mysql")

View File

@ -20,9 +20,10 @@
package dev.dnpm.etl.processor package dev.dnpm.etl.processor
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
@SpringBootApplication @SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
class EtlProcessorApplication class EtlProcessorApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {

View File

@ -1,7 +1,7 @@
/* /*
* This file is part of ETL-Processor * 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 * 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 * 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 { enum class PseudonymGenerator {
BUILDIN, BUILDIN,
GPAS 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 topic: test
response-topic: test_response response-topic: test_response
servers: localhost:9094 servers: localhost:9094
#security:
# admin-user: admin
# admin-password: very-secret
server: server:
port: 8000 port: 8000

View File

@ -74,20 +74,25 @@ nav > ul {
nav > ul > li { nav > ul > li {
display: inline-block; display: inline-block;
padding: 0 1rem; padding: 0 1rem;
}
nav > ul > li.login {
margin: 0 0 0 1em;
padding: 0 0 0 2em;
border-left: 1px solid var(--table-border); border-left: 1px solid var(--table-border);
} }
nav > ul > li:first-of-type {
border-left: none;
}
nav li a { nav li a {
color: #004a8f; color: var(--bg-blue);
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
font-weight: 700; font-weight: 700;
} }
nav li.login a {
color: var(--bg-red);
}
nav li a:hover { nav li a:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -177,6 +182,39 @@ form.samplecode-input input:focus-visible {
background: lightgreen; 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 { .border {
padding: 1.5em; padding: 1.5em;
border: 1px solid var(--table-border); border: 1px solid var(--table-border);
@ -200,6 +238,7 @@ table {
} }
.border > table { .border > table {
padding: 0;
border: none; border: none;
background: transparent; background: transparent;
} }
@ -272,6 +311,13 @@ td > small {
text-align: center; text-align: center;
} }
td.patient-id {
width: 32em;
text-overflow: ellipsis;
overflow: hidden;
display: block;
}
td.bg-blue, th.bg-blue, td.bg-blue, th.bg-blue,
td.bg-green, th.bg-green, td.bg-green, th.bg-green,
td.bg-yellow, th.bg-yellow, td.bg-yellow, th.bg-yellow,
@ -460,3 +506,18 @@ input.inline:focus-visible {
.connection-display .connection.available { .connection-display .connection.available {
background: var(--bg-green); 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> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" th:href="@{/style.css}" /> <link rel="stylesheet" th:href="@{/style.css}" />
@ -14,7 +14,15 @@
<ul> <ul>
<li><a th:href="@{/}">Übersicht</a></li> <li><a th:href="@{/}">Übersicht</a></li>
<li><a th:href="@{/statistics}">Statistiken</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> </ul>
</nav> </nav>
</div> </div>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!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> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ETL-Prozessor</title> <title>ETL-Prozessor</title>
@ -42,7 +42,8 @@
<a th:href="@{/report/{id}(id=${request.uuid})}">[[ ${request.uuid} ]]</a> <a th:href="@{/report/{id}(id=${request.uuid})}">[[ ${request.uuid} ]]</a>
</td> </td>
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></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> </tr>
</tbody> </tbody>
</table> </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> <!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> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ETL-Prozessor</title> <title>ETL-Prozessor</title>
@ -31,7 +31,8 @@
<td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td> <td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>
<td>[[ ${request.uuid} ]]</td> <td>[[ ${request.uuid} ]]</td>
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></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> </tr>
</tbody> </tbody>
</table> </table>