summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul-Christian Volkmer2026-03-07 10:48:16 +0100
committerGitHub2026-03-07 09:48:16 +0000
commitee5f9096c85f6789078597ba19f7c02e6b24d2c5 (patch)
tree616831069941510eea5ce652947837adefe86e49
parent9eb8d74117c4c363f787fbc3e02a90e7f21a402e (diff)
feat: configuration of additional users (#254)
-rw-r--r--README.md25
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt13
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt6
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt53
4 files changed, 59 insertions, 38 deletions
diff --git a/README.md b/README.md
index 1e8722d..9c8a7c0 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,13 @@ Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in
### 🔥 Wichtige Änderungen in Version 0.15
+#### Konfiguration von Benutzern
+
+Zusätzlich zu einem Administrator-Account können nun [weitere Benutzer](#weitere-benutzer)
+mit nicht administrativen Rechten in der Konfiguration angelegt werden.
+
+#### TAN-Speicherung
+
Ab Version 0.15 wird zu jeder Anfrage die generierte TAN zusätzlich zur Request-ID gespeichert.
Die TAN wird nur für MTB-Anfragen gespeichert, da sie für Lösch-Anfragen nicht relevant ist.
@@ -222,8 +229,22 @@ Hier Beispiele für das Beispielpasswort `very-secret`:
* `{sha256}9a34717f0646b5e9cfcba70055de62edb026ff4f68671ba3db96aa29297d2df5f1a037d58c745657`
Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der
-Anwendung in den Logs
-angezeigt.
+Anwendung in den Logs angezeigt.
+
+#### Weitere Benutzer
+
+Ab Version 0.15.0 können weitere Benutzer-Accounts konfiguriert werden, ohne OpenID Connect zu verwenden.
+Diese haben lediglich Benutzerrechte und können keine Konfigurationen einsehen oder ändern.
+
+Beispiele:
+
+```
+APP_SECURITY_USERS[0]_USERNAME=myuser
+APP_SECURITY_USERS[0]_PASSWORD={noop}very-secret
+APP_SECURITY_USERS[1]_USERNAME=otheruser
+APP_SECURITY_USERS[1]_PASSWORD={bcrypt}$2y$05$CCkfsMr/wbTleMyjVIK8g.Aa3RCvrvoLXVAsL.f6KeouS88vXD9b6
+...
+```
#### Weitere (nicht administrative) Nutzer mit OpenID Connect
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
index afaca73..4c7de9c 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
@@ -159,16 +159,16 @@ class MtbFileRestControllerTest {
"/api/mtb/etl/patient-record",
]
)
- fun testShouldDenyPermissionToSendMtbFile(url: String) {
+ fun testShouldGrantPermissionToSendMtbFileToUser(url: String) {
mockMvc
.post(url) {
- with(anonymous())
+ with(user("testuser").roles("USER"))
contentType = MediaType.APPLICATION_JSON
content = ObjectMapper().writeValueAsString(mtbFile)
}
- .andExpect { status { isUnauthorized() } }
+ .andExpect { status { isAccepted() } }
- verify(requestProcessor, never()).processMtbFile(any<Mtb>())
+ verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
}
@ParameterizedTest
@@ -185,14 +185,13 @@ class MtbFileRestControllerTest {
"/api/mtb/etl/patient-record",
]
)
- fun testShouldDenyPermissionToSendMtbFileForUser(url: String) {
+ fun testShouldDenyPermissionToSendMtbFileForAnonymous(url: String) {
mockMvc
.post(url) {
- with(user("fakeuser").roles("USER"))
contentType = MediaType.APPLICATION_JSON
content = ObjectMapper().writeValueAsString(mtbFile)
}
- .andExpect { status { isForbidden() } }
+ .andExpect { status { isUnauthorized() } }
verify(requestProcessor, never()).processMtbFile(any<Mtb>())
}
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 d2922f2..63f50a6 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -136,12 +136,18 @@ data class SecurityConfigProperties(
val enableTokens: Boolean = false,
val enableOidc: Boolean = false,
val defaultNewUserRole: Role = Role.USER,
+ val users: List<UserProperties> = listOf(),
) {
companion object {
const val NAME = "app.security"
}
}
+data class UserProperties(
+ val username: String,
+ val password: String,
+)
+
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
index 9b48d22..60b1a9c 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
@@ -53,6 +53,22 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
private val logger = LoggerFactory.getLogger(AppSecurityConfiguration::class.java)
+ private fun authorizeAppRequests(http: HttpSecurity) {
+ http {
+ authorizeHttpRequests {
+ authorize("/configs/**", hasRole("ADMIN"))
+ authorize("/api/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
+ authorize("/api/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
+ authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
+ authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
+ authorize("/patient/**", hasAnyRole("ADMIN", "USER"))
+ authorize("/report/**", hasAnyRole("ADMIN", "USER"))
+ authorize("/submission/**", hasAnyRole("ADMIN", "USER"))
+ authorize(anyRequest, permitAll)
+ }
+ }
+ }
+
@Bean
fun userDetailsService(passwordEncoder: PasswordEncoder): InMemoryUserDetailsManager {
val adminUser =
@@ -72,10 +88,14 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
securityConfigProperties.adminPassword
}
- val user: UserDetails =
+ val admin: UserDetails =
User.withUsername(adminUser).password(adminPassword).roles("ADMIN").build()
- return InMemoryUserDetailsManager(user)
+ val users = securityConfigProperties.users.map {
+ User.withUsername(it.username).password(it.password).roles("USER").build()
+ }.toTypedArray()
+
+ return InMemoryUserDetailsManager(admin, *users)
}
@Bean
@@ -86,24 +106,8 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
userRoleRepository: UserRoleRepository,
sessionRegistry: SessionRegistry,
): SecurityFilterChain {
+ authorizeAppRequests(http)
http {
- authorizeHttpRequests {
- authorize("/configs/**", hasRole("ADMIN"))
- authorize("/api/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
- authorize("/api/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
- authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
- authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
- authorize("/report/**", hasAnyRole("ADMIN", "USER"))
- authorize("/submission/**", hasAnyRole("ADMIN", "USER"))
- authorize("/**/*.css", permitAll)
- authorize("/**/*.ico", permitAll)
- authorize("/**/*.jpeg", permitAll)
- authorize("/**/*.js", permitAll)
- authorize("/**/*.svg", permitAll)
- authorize("/**/*.css", permitAll)
- authorize("/login/**", permitAll)
- authorize(anyRequest, permitAll)
- }
httpBasic { realmName = "ETL-Processor" }
formLogin { loginPage = LOGIN_PATH }
oauth2Login { loginPage = LOGIN_PATH }
@@ -154,17 +158,8 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
matchIfMissing = true,
)
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
+ authorizeAppRequests(http)
http {
- authorizeHttpRequests {
- authorize("/configs/**", hasRole("ADMIN"))
- authorize("/api/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
- authorize("/api/mtb/**", hasAnyRole("MTBFILE", "ADMIN"))
- authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
- authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN"))
- authorize("/report/**", hasRole("ADMIN"))
- authorize("/submission/**", hasAnyRole("ADMIN"))
- authorize(anyRequest, permitAll)
- }
httpBasic { realmName = "ETL-Processor" }
formLogin { loginPage = LOGIN_PATH }
csrf { disable() }