summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md18
-rw-r--r--build.gradle.kts2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/EtlProcessorApplication.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt98
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt33
-rw-r--r--src/main/resources/application-dev.yml3
-rw-r--r--src/main/resources/static/style.css69
-rw-r--r--src/main/resources/templates/fragments.html12
-rw-r--r--src/main/resources/templates/index.html5
-rw-r--r--src/main/resources/templates/login.html23
-rw-r--r--src/main/resources/templates/report.html5
12 files changed, 271 insertions, 12 deletions
diff --git a/README.md b/README.md
index 9ebe9e8..f3fe79f 100644
--- a/README.md
+++ b/README.md
@@ -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>