1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-04 15:32:55 +00:00

feat: add config page for user role assignment

This commit is contained in:
2024-03-01 13:51:06 +01:00
parent 200c5338ea
commit feb9f2430c
8 changed files with 197 additions and 8 deletions

View File

@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.config
import dev.dnpm.etl.processor.security.UserRole
import dev.dnpm.etl.processor.security.UserRoleRepository
import dev.dnpm.etl.processor.services.UserRoleService
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
@ -31,6 +32,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.core.session.SessionRegistryImpl
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.crypto.factory.PasswordEncoderFactories
@ -82,7 +85,7 @@ class AppSecurityConfiguration(
@Bean
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
fun filterChainOidc(http: HttpSecurity, passwordEncoder: PasswordEncoder, userRoleRepository: UserRoleRepository): SecurityFilterChain {
fun filterChainOidc(http: HttpSecurity, passwordEncoder: PasswordEncoder, userRoleRepository: UserRoleRepository, sessionRegistry: SessionRegistry): SecurityFilterChain {
http {
authorizeRequests {
authorize("/configs/**", hasRole("ADMIN"))
@ -95,7 +98,7 @@ class AppSecurityConfiguration(
authorize("*.svg", permitAll)
authorize("*.css", permitAll)
authorize("/login/**", permitAll)
authorize(anyRequest, fullyAuthenticated)
authorize(anyRequest, permitAll)
}
httpBasic {
realmName = "ETL-Processor"
@ -106,6 +109,16 @@ class AppSecurityConfiguration(
oauth2Login {
loginPage = "/login"
}
sessionManagement {
sessionConcurrency {
maximumSessions = 1
maxSessionsPreventsLogin = true
expiredUrl = "/login?expired"
}
sessionFixation {
newSession()
}
}
csrf { disable() }
}
return http.build()
@ -150,9 +163,19 @@ class AppSecurityConfiguration(
return http.build()
}
@Bean
fun sessionRegistry(): SessionRegistry {
return SessionRegistryImpl()
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
@Bean
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
fun userRoleService(userRoleRepository: UserRoleRepository, sessionRegistry: SessionRegistry): UserRoleService {
return UserRoleService(userRoleRepository, sessionRegistry)
}
}

View File

@ -23,13 +23,13 @@ package dev.dnpm.etl.processor.security
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
import java.util.Optional
import java.util.*
@Table("user_role")
data class UserRole(
@Id val id: Long? = null,
val username: String,
val role: Role = Role.GUEST
var role: Role = Role.GUEST
)
enum class Role(val value: String) {

View File

@ -0,0 +1,61 @@
/*
* 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.services
import dev.dnpm.etl.processor.security.Role
import dev.dnpm.etl.processor.security.UserRole
import dev.dnpm.etl.processor.security.UserRoleRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.oauth2.core.oidc.user.OidcUser
class UserRoleService(
private val userRoleRepository: UserRoleRepository,
private val sessionRegistry: SessionRegistry
) {
fun updateUserRole(id: Long, role: Role) {
val userRole = userRoleRepository.findByIdOrNull(id) ?: return
userRole.role = role
userRoleRepository.save(userRole)
expireSessionFor(userRole.username)
}
fun deleteUserRole(id: Long) {
val userRole = userRoleRepository.findByIdOrNull(id) ?: return
userRoleRepository.delete(userRole)
expireSessionFor(userRole.username)
}
fun findAll(): List<UserRole> {
return userRoleRepository.findAll().toList()
}
private fun expireSessionFor(username: String) {
sessionRegistry.allPrincipals
.filterIsInstance<OidcUser>()
.filter { it.preferredUsername == username }
.flatMap {
sessionRegistry.getAllSessions(it, true)
}
.onEach {
it.expireNow()
}
}
}

View File

@ -22,9 +22,12 @@ package dev.dnpm.etl.processor.web
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
import dev.dnpm.etl.processor.output.MtbFileSender
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.security.Role
import dev.dnpm.etl.processor.security.UserRole
import dev.dnpm.etl.processor.services.Token
import dev.dnpm.etl.processor.services.TokenService
import dev.dnpm.etl.processor.services.TransformationService
import dev.dnpm.etl.processor.services.UserRoleService
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.MediaType
import org.springframework.http.codec.ServerSentEvent
@ -43,7 +46,8 @@ class ConfigController(
private val pseudonymGenerator: Generator,
private val mtbFileSender: MtbFileSender,
private val connectionCheckService: ConnectionCheckService,
private val tokenService: TokenService?
private val tokenService: TokenService?,
private val userRoleService: UserRoleService?
) {
@GetMapping
@ -56,10 +60,16 @@ class ConfigController(
if (tokenService != null) {
model.addAttribute("tokens", tokenService.findAll())
} else {
model.addAttribute("tokens", listOf<Token>())
model.addAttribute("tokens", emptyList<Token>())
}
model.addAttribute("transformations", transformationService.getTransformations())
if (userRoleService != null) {
model.addAttribute("userRolesEnabled", true)
model.addAttribute("userRoles", userRoleService.findAll())
} else {
model.addAttribute("userRolesEnabled", false)
model.addAttribute("userRoles", emptyList<UserRole>())
}
return "configs"
}
@ -112,6 +122,34 @@ class ConfigController(
return "configs/tokens"
}
@DeleteMapping(path = ["userroles/{id}"])
fun deleteUserRole(@PathVariable id: Long, model: Model): String {
if (userRoleService != null) {
userRoleService.deleteUserRole(id)
model.addAttribute("userRolesEnabled", true)
model.addAttribute("userRoles", userRoleService.findAll())
} else {
model.addAttribute("userRolesEnabled", false)
model.addAttribute("userRoles", emptyList<UserRole>())
}
return "configs/userroles"
}
@PutMapping(path = ["userroles/{id}"])
fun updateUserRole(@PathVariable id: Long, @ModelAttribute("role") role: Role, model: Model): String {
if (userRoleService != null) {
userRoleService.updateUserRole(id, role)
model.addAttribute("userRolesEnabled", true)
model.addAttribute("userRoles", userRoleService.findAll())
} else {
model.addAttribute("userRolesEnabled", false)
model.addAttribute("userRoles", emptyList<UserRole>())
}
return "configs/userroles"
}
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun events(): Flux<ServerSentEvent<Any>> {
return configsUpdateProducer.asFlux().map {