diff options
| author | Paul-Christian Volkmer | 2024-01-18 14:29:52 +0100 |
|---|---|---|
| committer | GitHub | 2024-01-18 14:29:52 +0100 |
| commit | 358373cf70212b97044e9714ad1b913fcd24e6bc (patch) | |
| tree | edabf0677d519437dfe78bdb0c8cd7ba2d217fd3 /src/main/kotlin/dev | |
| parent | 531a8589db2bf170e6272602ccb4a3c4457186d8 (diff) | |
| parent | 27a62321faec1087048e624fe68ab53c4fdbc0a3 (diff) | |
Merge pull request #30 from CCC-MF/issue_29
Issue #29: Unterstützung für Endpoint-Tokens
Diffstat (limited to 'src/main/kotlin/dev')
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) |
