mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-07-04 15:32:55 +00:00
feat #29: add initial support for mtbfile api tokens
This commit is contained in:
@ -86,6 +86,7 @@ data class KafkaTargetProperties(
|
||||
data class SecurityConfigProperties(
|
||||
val adminUser: String?,
|
||||
val adminPassword: String?,
|
||||
val enableTokens: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
const val NAME = "app.security"
|
||||
|
@ -25,6 +25,8 @@ import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||
import dev.dnpm.etl.processor.services.TokenRepository
|
||||
import dev.dnpm.etl.processor.services.TokenService
|
||||
import dev.dnpm.etl.processor.services.Transformation
|
||||
import dev.dnpm.etl.processor.services.TransformationService
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -37,6 +39,9 @@ import org.springframework.retry.policy.SimpleRetryPolicy
|
||||
import org.springframework.retry.support.RetryTemplate
|
||||
import org.springframework.retry.support.RetryTemplateBuilder
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.provisioning.UserDetailsManager
|
||||
import reactor.core.publisher.Sinks
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
@ -114,6 +119,12 @@ class AppConfiguration {
|
||||
.build()
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true")
|
||||
@Bean
|
||||
fun tokenService(userDetailsManager: InMemoryUserDetailsManager, passwordEncoder: PasswordEncoder, tokenRepository: TokenRepository): TokenService {
|
||||
return TokenService(userDetailsManager, passwordEncoder, tokenRepository)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun statisticsUpdateProducer(): Sinks.Many<Any> {
|
||||
return Sinks.many().multicast().directBestEffort()
|
||||
|
@ -24,15 +24,21 @@ 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.core.Ordered
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
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.config.http.SessionCreationPolicy
|
||||
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 org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
|
||||
import java.util.*
|
||||
|
||||
|
||||
@ -76,12 +82,16 @@ class AppSecurityConfiguration(
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize("/configs/**", hasRole("ADMIN"))
|
||||
authorize("/mtbfile/**", hasAnyRole("MTBFILE"))
|
||||
authorize(anyRequest, permitAll)
|
||||
}
|
||||
httpBasic {
|
||||
realmName = "ETL-Processor"
|
||||
}
|
||||
formLogin {
|
||||
loginPage = "/login"
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 jakarta.annotation.PostConstruct
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
import org.springframework.data.repository.CrudRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class TokenService(
|
||||
private val userDetailsManager: InMemoryUserDetailsManager,
|
||||
private val passwordEncoder: PasswordEncoder,
|
||||
private val tokenRepository: TokenRepository
|
||||
) {
|
||||
|
||||
@PostConstruct
|
||||
fun setup() {
|
||||
tokenRepository.findAll().forEach {
|
||||
userDetailsManager.createUser(
|
||||
User.withUsername(it.username)
|
||||
.password(it.password)
|
||||
.roles("MTBFILE")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addToken(name: String): Result<String> {
|
||||
val username = name.lowercase().replace("""[^a-z0-9]""".toRegex(), "")
|
||||
if (userDetailsManager.userExists(username)) {
|
||||
return Result.failure(RuntimeException("Cannot use token name"))
|
||||
}
|
||||
|
||||
val password = Base64.getEncoder().encodeToString(UUID.randomUUID().toString().encodeToByteArray())
|
||||
val encodedPassword = passwordEncoder.encode(password).toString()
|
||||
|
||||
userDetailsManager.createUser(
|
||||
User.withUsername(username)
|
||||
.password(encodedPassword)
|
||||
.roles("MTBFILE")
|
||||
.build()
|
||||
)
|
||||
|
||||
tokenRepository.save(Token(name = name, username = username, password = encodedPassword))
|
||||
|
||||
return Result.success("$username:$password")
|
||||
}
|
||||
|
||||
fun deleteToken(id: Long) {
|
||||
val token = tokenRepository.findByIdOrNull(id) ?: return
|
||||
userDetailsManager.deleteUser(token.username)
|
||||
tokenRepository.delete(token)
|
||||
}
|
||||
|
||||
fun findAll(): List<Token> {
|
||||
return tokenRepository.findAll().toList()
|
||||
}
|
||||
}
|
||||
|
||||
@Table("token")
|
||||
data class Token(
|
||||
@Id val id: Long? = null,
|
||||
val name: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val createdAt: Instant = Instant.now()
|
||||
)
|
||||
|
||||
interface TokenRepository : CrudRepository<Token, Long>
|
@ -22,14 +22,15 @@ 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.services.Token
|
||||
import dev.dnpm.etl.processor.services.TokenService
|
||||
import dev.dnpm.etl.processor.services.TransformationService
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.codec.ServerSentEvent
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
|
||||
@ -41,8 +42,8 @@ class ConfigController(
|
||||
private val transformationService: TransformationService,
|
||||
private val pseudonymGenerator: Generator,
|
||||
private val mtbFileSender: MtbFileSender,
|
||||
private val connectionCheckService: ConnectionCheckService
|
||||
|
||||
private val connectionCheckService: ConnectionCheckService,
|
||||
private val tokenService: TokenService?
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
@ -51,6 +52,12 @@ class ConfigController(
|
||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
||||
model.addAttribute("connectionAvailable", connectionCheckService.connectionAvailable())
|
||||
model.addAttribute("tokensEnabled", tokenService != null)
|
||||
if (tokenService != null) {
|
||||
model.addAttribute("tokens", tokenService.findAll())
|
||||
} else {
|
||||
model.addAttribute("tokens", listOf<Token>())
|
||||
}
|
||||
model.addAttribute("transformations", transformationService.getTransformations())
|
||||
|
||||
return "configs"
|
||||
@ -61,10 +68,50 @@ class ConfigController(
|
||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
||||
model.addAttribute("connectionAvailable", connectionCheckService.connectionAvailable())
|
||||
if (tokenService != null) {
|
||||
model.addAttribute("tokensEnabled", true)
|
||||
model.addAttribute("tokens", tokenService.findAll())
|
||||
} else {
|
||||
model.addAttribute("tokens", listOf<Token>())
|
||||
}
|
||||
|
||||
return "configs/connectionAvailable"
|
||||
}
|
||||
|
||||
@PostMapping(path = ["tokens"])
|
||||
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
|
||||
if (tokenService == null) {
|
||||
model.addAttribute("tokensEnabled", false)
|
||||
model.addAttribute("success", false)
|
||||
} else {
|
||||
model.addAttribute("tokensEnabled", true)
|
||||
val result = tokenService.addToken(name)
|
||||
if (result.isSuccess) {
|
||||
model.addAttribute("newTokenValue", result.getOrDefault(""))
|
||||
model.addAttribute("success", true)
|
||||
} else {
|
||||
model.addAttribute("success", false)
|
||||
}
|
||||
model.addAttribute("tokens", tokenService.findAll())
|
||||
}
|
||||
|
||||
return "configs/tokens"
|
||||
}
|
||||
|
||||
@DeleteMapping(path = ["tokens/{id}"])
|
||||
fun deleteToken(@PathVariable id: Long, model: Model): String {
|
||||
if (tokenService != null) {
|
||||
tokenService.deleteToken(id)
|
||||
|
||||
model.addAttribute("tokensEnabled", true)
|
||||
model.addAttribute("tokens", tokenService.findAll())
|
||||
} else {
|
||||
model.addAttribute("tokensEnabled", false)
|
||||
model.addAttribute("tokens", listOf<Token>())
|
||||
}
|
||||
return "configs/tokens"
|
||||
}
|
||||
|
||||
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
|
||||
fun events(): Flux<ServerSentEvent<Any>> {
|
||||
return configsUpdateProducer.asFlux().map {
|
||||
|
@ -27,13 +27,19 @@ import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = ["mtbfile"])
|
||||
class MtbFileRestController(
|
||||
private val requestProcessor: RequestProcessor,
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
||||
|
||||
@PostMapping(path = ["/mtbfile"])
|
||||
@GetMapping
|
||||
fun info(): ResponseEntity<String> {
|
||||
return ResponseEntity.ok("Test")
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Void> {
|
||||
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
||||
logger.debug("Accepted MTB File for processing")
|
||||
@ -45,7 +51,7 @@ class MtbFileRestController(
|
||||
return ResponseEntity.accepted().build()
|
||||
}
|
||||
|
||||
@DeleteMapping(path = ["/mtbfile/{patientId}"])
|
||||
@DeleteMapping(path = ["{patientId}"])
|
||||
fun deleteData(@PathVariable patientId: String): ResponseEntity<Void> {
|
||||
logger.debug("Accepted patient ID to process deletion")
|
||||
requestProcessor.processDeletion(patientId)
|
||||
|
Reference in New Issue
Block a user