summaryrefslogtreecommitdiff
path: root/src/main/kotlin/dev/dnpm/etl
diff options
context:
space:
mode:
authorPaul-Christian Volkmer2024-01-18 14:13:15 +0100
committerPaul-Christian Volkmer2024-01-18 14:13:15 +0100
commit30cf0fd22e492fd2b0052ddfd5b808da51b36052 (patch)
tree5745f1a2be493e9149cc986833d904bd62e01bfc /src/main/kotlin/dev/dnpm/etl
parent531a8589db2bf170e6272602ccb4a3c4457186d8 (diff)
feat #29: add initial support for mtbfile api tokens
Diffstat (limited to 'src/main/kotlin/dev/dnpm/etl')
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt1
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt11
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt92
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt55
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/MtbFileRestController.kt10
6 files changed, 174 insertions, 7 deletions
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 08a45bb..aacf97d 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -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"
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
index 83cc568..92965a6 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -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()
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 e0cff94..22a2e34 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
@@ -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"
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt
new file mode 100644
index 0000000..f084408
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt
@@ -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> \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
index be291ea..dbedee5 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
@@ -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 {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/MtbFileRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/MtbFileRestController.kt
index 9b441f6..d417a1f 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/MtbFileRestController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/MtbFileRestController.kt
@@ -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)