From 3bc148f7eaf8531e28900ca080795dc2a68753ac Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Tue, 7 May 2024 08:58:00 +0200 Subject: refactor: move classes into package 'security' --- .../etl/processor/config/AppConfigurationTest.kt | 4 +- .../processor/input/MtbFileRestControllerTest.kt | 2 +- .../dnpm/etl/processor/web/ConfigControllerTest.kt | 4 +- .../dnpm/etl/processor/web/LoginControllerTest.kt | 2 +- .../dnpm/etl/processor/config/AppConfiguration.kt | 4 +- .../processor/config/AppSecurityConfiguration.kt | 2 +- .../dnpm/etl/processor/security/TokenService.kt | 92 +++++++++ .../dnpm/etl/processor/security/UserRoleService.kt | 58 ++++++ .../dnpm/etl/processor/services/TokenService.kt | 92 --------- .../dnpm/etl/processor/services/UserRoleService.kt | 61 ------ .../dev/dnpm/etl/processor/web/ConfigController.kt | 6 +- .../etl/processor/security/TokenServiceTest.kt | 154 ++++++++++++++++ .../etl/processor/security/UserRoleServiceTest.kt | 202 ++++++++++++++++++++ .../etl/processor/services/TokenServiceTest.kt | 154 ---------------- .../etl/processor/services/UserRoleServiceTest.kt | 205 --------------------- 15 files changed, 518 insertions(+), 524 deletions(-) create mode 100644 src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt create mode 100644 src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt delete mode 100644 src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt delete mode 100644 src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt create mode 100644 src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt create mode 100644 src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt delete mode 100644 src/test/kotlin/dev/dnpm/etl/processor/services/TokenServiceTest.kt delete mode 100644 src/test/kotlin/dev/dnpm/etl/processor/services/UserRoleServiceTest.kt diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt index 262aca0..c7454ed 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt @@ -27,8 +27,8 @@ import dev.dnpm.etl.processor.output.RestMtbFileSender import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator import dev.dnpm.etl.processor.services.RequestProcessor -import dev.dnpm.etl.processor.services.TokenRepository -import dev.dnpm.etl.processor.services.TokenService +import dev.dnpm.etl.processor.security.TokenRepository +import dev.dnpm.etl.processor.security.TokenService import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test 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 f1586d0..d8c1321 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import dev.dnpm.etl.processor.config.AppSecurityConfiguration import dev.dnpm.etl.processor.services.RequestProcessor -import dev.dnpm.etl.processor.services.TokenRepository +import dev.dnpm.etl.processor.security.TokenRepository import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt index cef63d8..9c7ae3e 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt @@ -29,9 +29,9 @@ 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.services.RequestProcessor -import dev.dnpm.etl.processor.services.TokenService +import dev.dnpm.etl.processor.security.TokenService import dev.dnpm.etl.processor.services.TransformationService -import dev.dnpm.etl.processor.services.UserRoleService +import dev.dnpm.etl.processor.security.UserRoleService import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt index 83cf1b8..0471543 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt @@ -23,7 +23,7 @@ import com.gargoylesoftware.htmlunit.WebClient import com.gargoylesoftware.htmlunit.html.HtmlPage import dev.dnpm.etl.processor.config.AppConfiguration import dev.dnpm.etl.processor.config.AppSecurityConfiguration -import dev.dnpm.etl.processor.services.TokenService +import dev.dnpm.etl.processor.security.TokenService import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test 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 6ebcadd..5fc1120 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -28,8 +28,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.security.TokenRepository +import dev.dnpm.etl.processor.security.TokenService import dev.dnpm.etl.processor.services.Transformation import dev.dnpm.etl.processor.services.TransformationService import org.apache.hc.client5.http.impl.classic.HttpClients 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 c377555..0da9398 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt @@ -21,7 +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 dev.dnpm.etl.processor.security.UserRoleService import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.EnableConfigurationProperties diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt new file mode 100644 index 0000000..44b04e8 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/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 . + */ + +package dev.dnpm.etl.processor.security + +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 { + 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 { + 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 \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt new file mode 100644 index 0000000..174f8a9 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt @@ -0,0 +1,58 @@ +/* + * 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 . + */ + +package dev.dnpm.etl.processor.security + +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 { + return userRoleRepository.findAll().toList() + } + + private fun expireSessionFor(username: String) { + sessionRegistry.allPrincipals + .filterIsInstance() + .filter { it.preferredUsername == username } + .flatMap { + sessionRegistry.getAllSessions(it, true) + } + .onEach { + it.expireNow() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt deleted file mode 100644 index f084408..0000000 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 . - */ - -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 { - 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 { - 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 \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt deleted file mode 100644 index 6649f7d..0000000 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 . - */ - -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 { - return userRoleRepository.findAll().toList() - } - - private fun expireSessionFor(username: String) { - sessionRegistry.allPrincipals - .filterIsInstance() - .filter { it.preferredUsername == username } - .flatMap { - sessionRegistry.getAllSessions(it, true) - } - .onEach { - it.expireNow() - } - } -} \ 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 4e101eb..165535f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt @@ -27,10 +27,10 @@ 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.security.Token +import dev.dnpm.etl.processor.security.TokenService import dev.dnpm.etl.processor.services.TransformationService -import dev.dnpm.etl.processor.services.UserRoleService +import dev.dnpm.etl.processor.security.UserRoleService import org.springframework.beans.factory.annotation.Qualifier import org.springframework.http.MediaType import org.springframework.http.codec.ServerSentEvent diff --git a/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt new file mode 100644 index 0000000..e8f4e04 --- /dev/null +++ b/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt @@ -0,0 +1,154 @@ +/* + * 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 . + */ + +package dev.dnpm.etl.processor.security + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import java.util.* +import java.util.function.Consumer + +@ExtendWith(MockitoExtension::class) +class TokenServiceTest { + + private lateinit var userDetailsManager: InMemoryUserDetailsManager + private lateinit var passwordEncoder: PasswordEncoder + private lateinit var tokenRepository: TokenRepository + + private lateinit var tokenService: TokenService + + @BeforeEach + fun setup( + @Mock userDetailsManager: InMemoryUserDetailsManager, + @Mock passwordEncoder: PasswordEncoder, + @Mock tokenRepository: TokenRepository + ) { + this.userDetailsManager = userDetailsManager + this.passwordEncoder = passwordEncoder + this.tokenRepository = tokenRepository + + this.tokenService = TokenService(userDetailsManager, passwordEncoder, tokenRepository) + } + + @Test + fun shouldEncodePasswordForNewToken() { + doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) + + val actual = this.tokenService.addToken("Test Token") + + assertThat(actual).satisfies( + Consumer { assertThat(it.isSuccess).isTrue() }, + Consumer { assertThat(it.getOrNull()).matches("testtoken:[A-Za-z0-9]{48}$") } + ) + } + + @Test + fun shouldContainAlphanumTokenUserPart() { + doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) + + val actual = this.tokenService.addToken("Test Token") + + assertThat(actual).satisfies( + Consumer { assertThat(it.isSuccess).isTrue() }, + Consumer { assertThat(it.getOrDefault("")).startsWith("testtoken:") } + ) + } + + @Test + fun shouldNotAllowSameTokenUserPartTwice() { + doReturn(true).whenever(userDetailsManager).userExists(anyString()) + + val actual = this.tokenService.addToken("Test Token") + + assertThat(actual).satisfies(Consumer { assertThat(it.isFailure).isTrue() }) + verify(tokenRepository, never()).save(any()) + } + + @Test + fun shouldSaveNewToken() { + doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) + + val actual = this.tokenService.addToken("Test Token") + + val captor = ArgumentCaptor.forClass(Token::class.java) + verify(tokenRepository, times(1)).save(captor.capture()) + + assertThat(actual).satisfies(Consumer { assertThat(it.isSuccess).isTrue() }) + assertThat(captor.value).satisfies( + Consumer { assertThat(it.name).isEqualTo("Test Token") }, + Consumer { assertThat(it.username).isEqualTo("testtoken") }, + Consumer { assertThat(it.password).isEqualTo("{test}verysecret") } + ) + } + + @Test + fun shouldDeleteExistingToken() { + doAnswer { + val id = it.arguments[0] as Long + Optional.of(Token(id, "Test Token", "testtoken", "{test}hsdajfgadskjhfgsdkfjg")) + }.whenever(tokenRepository).findById(anyLong()) + + this.tokenService.deleteToken(42) + + val stringCaptor = ArgumentCaptor.forClass(String::class.java) + verify(userDetailsManager, times(1)).deleteUser(stringCaptor.capture()) + assertThat(stringCaptor.value).isEqualTo("testtoken") + + val tokenCaptor = ArgumentCaptor.forClass(Token::class.java) + verify(tokenRepository, times(1)).delete(tokenCaptor.capture()) + assertThat(tokenCaptor.value.id).isEqualTo(42) + } + + @Test + fun shouldReturnAllTokensFromRepository() { + val expected = listOf( + Token(1, "Test Token 1", "testtoken1", "{test}hsdajfgadskjhfgsdkfjg"), + Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd") + ) + + doReturn(expected).whenever(tokenRepository).findAll() + + assertThat(tokenService.findAll()).isEqualTo(expected) + } + + @Test + fun shouldAddAllTokensFromRepositoryToUserDataManager() { + val expected = listOf( + Token(1, "Test Token 1", "testtoken1", "{test}hsdajfgadskjhfgsdkfjg"), + Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd") + ) + + doReturn(expected).whenever(tokenRepository).findAll() + + tokenService.setup() + + verify(userDetailsManager, times(expected.size)).createUser(any()) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt new file mode 100644 index 0000000..39ba7c0 --- /dev/null +++ b/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt @@ -0,0 +1,202 @@ +/* + * 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 . + */ + +package dev.dnpm.etl.processor.security + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import org.springframework.security.core.session.SessionInformation +import org.springframework.security.core.session.SessionRegistry +import org.springframework.security.oauth2.core.oidc.OidcIdToken +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser +import org.springframework.security.oauth2.core.oidc.user.OidcUser +import java.time.Instant +import java.util.* + +@ExtendWith(MockitoExtension::class) +class UserRoleServiceTest { + + private lateinit var userRoleRepository: UserRoleRepository + private lateinit var sessionRegistry: SessionRegistry + + private lateinit var userRoleService: UserRoleService + + @BeforeEach + fun setup( + @Mock userRoleRepository: UserRoleRepository, + @Mock sessionRegistry: SessionRegistry + ) { + this.userRoleRepository = userRoleRepository + this.sessionRegistry = sessionRegistry + + this.userRoleService = UserRoleService(userRoleRepository, sessionRegistry) + } + + @Test + fun shouldDelegateFindAllToRepository() { + userRoleService.findAll() + + verify(userRoleRepository, times(1)).findAll() + } + + @Nested + inner class WithExistingUserRole { + + @BeforeEach + fun setup() { + doAnswer { invocation -> + Optional.of( + UserRole(invocation.getArgument(0), "patrick.tester", Role.USER) + ) + }.whenever(userRoleRepository).findById(any()) + + doAnswer { _ -> + listOf( + dummyPrincipal() + ) + }.whenever(sessionRegistry).allPrincipals + } + + @Test + fun shouldUpdateUserRole() { + userRoleService.updateUserRole(1, Role.ADMIN) + + val userRoleCaptor = argumentCaptor() + verify(userRoleRepository, times(1)).save(userRoleCaptor.capture()) + + assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) + assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.ADMIN) + } + + @Test + fun shouldExpireSessionOnUpdate() { + val dummySessions = dummySessions() + whenever(sessionRegistry.getAllSessions(any(), any())).thenReturn( + dummySessions + ) + + assertThat(dummySessions.filter { it.isExpired }).hasSize(0) + + userRoleService.updateUserRole(1, Role.ADMIN) + + verify(sessionRegistry, times(1)).getAllSessions(any(), any()) + + assertThat(dummySessions.filter { it.isExpired }).hasSize(2) + } + + @Test + fun shouldDeleteUserRole() { + userRoleService.deleteUserRole(1) + + val userRoleCaptor = argumentCaptor() + verify(userRoleRepository, times(1)).delete(userRoleCaptor.capture()) + + assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) + assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.USER) + } + + @Test + fun shouldExpireSessionOnDelete() { + val dummySessions = dummySessions() + whenever(sessionRegistry.getAllSessions(any(), any())).thenReturn( + dummySessions + ) + + assertThat(dummySessions.filter { it.isExpired }).hasSize(0) + + userRoleService.deleteUserRole(1) + + verify(sessionRegistry, times(1)).getAllSessions(any(), any()) + + assertThat(dummySessions.filter { it.isExpired }).hasSize(2) + } + } + + @Nested + inner class WithoutExistingUserRole { + + @BeforeEach + fun setup() { + doAnswer { _ -> + Optional.empty() + }.whenever(userRoleRepository).findById(any()) + } + + @Test + fun shouldNotUpdateUserRole() { + userRoleService.updateUserRole(1, Role.ADMIN) + + verify(userRoleRepository, never()).save(any()) + } + + @Test + fun shouldNotExpireSessionOnUpdate() { + userRoleService.updateUserRole(1, Role.ADMIN) + + verify(sessionRegistry, never()).getAllSessions(any(), any()) + } + + @Test + fun shouldNotDeleteUserRole() { + userRoleService.deleteUserRole(1) + + verify(userRoleRepository, never()).delete(any()) + } + + @Test + fun shouldNotExpireSessionOnDelete() { + userRoleService.deleteUserRole(1) + + verify(sessionRegistry, never()).getAllSessions(any(), any()) + } + + } + + + companion object { + private fun dummyPrincipal() = DefaultOidcUser( + listOf(), + OidcIdToken( + "anytokenvalue", + Instant.now(), + Instant.now().plusSeconds(10), + mapOf("sub" to "testsub", "preferred_username" to "patrick.tester") + ) + ) + + private fun dummySessions() = listOf( + SessionInformation( + dummyPrincipal(), + "SESSIONID1", + Date.from(Instant.now()), + ), + SessionInformation( + dummyPrincipal(), + "SESSIONID2", + Date.from(Instant.now()), + ) + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/TokenServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/TokenServiceTest.kt deleted file mode 100644 index 1fdc3d9..0000000 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/TokenServiceTest.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 . - */ - -package dev.dnpm.etl.processor.services - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyLong -import org.mockito.ArgumentMatchers.anyString -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.* -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.security.provisioning.InMemoryUserDetailsManager -import java.util.* -import java.util.function.Consumer - -@ExtendWith(MockitoExtension::class) -class TokenServiceTest { - - private lateinit var userDetailsManager: InMemoryUserDetailsManager - private lateinit var passwordEncoder: PasswordEncoder - private lateinit var tokenRepository: TokenRepository - - private lateinit var tokenService: TokenService - - @BeforeEach - fun setup( - @Mock userDetailsManager: InMemoryUserDetailsManager, - @Mock passwordEncoder: PasswordEncoder, - @Mock tokenRepository: TokenRepository - ) { - this.userDetailsManager = userDetailsManager - this.passwordEncoder = passwordEncoder - this.tokenRepository = tokenRepository - - this.tokenService = TokenService(userDetailsManager, passwordEncoder, tokenRepository) - } - - @Test - fun shouldEncodePasswordForNewToken() { - doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) - - val actual = this.tokenService.addToken("Test Token") - - assertThat(actual).satisfies( - Consumer { assertThat(it.isSuccess).isTrue() }, - Consumer { assertThat(it.getOrNull()).matches("testtoken:[A-Za-z0-9]{48}$") } - ) - } - - @Test - fun shouldContainAlphanumTokenUserPart() { - doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) - - val actual = this.tokenService.addToken("Test Token") - - assertThat(actual).satisfies( - Consumer { assertThat(it.isSuccess).isTrue() }, - Consumer { assertThat(it.getOrDefault("")).startsWith("testtoken:") } - ) - } - - @Test - fun shouldNotAllowSameTokenUserPartTwice() { - doReturn(true).whenever(userDetailsManager).userExists(anyString()) - - val actual = this.tokenService.addToken("Test Token") - - assertThat(actual).satisfies(Consumer { assertThat(it.isFailure).isTrue() }) - verify(tokenRepository, never()).save(any()) - } - - @Test - fun shouldSaveNewToken() { - doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) - - val actual = this.tokenService.addToken("Test Token") - - val captor = ArgumentCaptor.forClass(Token::class.java) - verify(tokenRepository, times(1)).save(captor.capture()) - - assertThat(actual).satisfies(Consumer { assertThat(it.isSuccess).isTrue() }) - assertThat(captor.value).satisfies( - Consumer { assertThat(it.name).isEqualTo("Test Token") }, - Consumer { assertThat(it.username).isEqualTo("testtoken") }, - Consumer { assertThat(it.password).isEqualTo("{test}verysecret") } - ) - } - - @Test - fun shouldDeleteExistingToken() { - doAnswer { - val id = it.arguments[0] as Long - Optional.of(Token(id, "Test Token", "testtoken", "{test}hsdajfgadskjhfgsdkfjg")) - }.whenever(tokenRepository).findById(anyLong()) - - this.tokenService.deleteToken(42) - - val stringCaptor = ArgumentCaptor.forClass(String::class.java) - verify(userDetailsManager, times(1)).deleteUser(stringCaptor.capture()) - assertThat(stringCaptor.value).isEqualTo("testtoken") - - val tokenCaptor = ArgumentCaptor.forClass(Token::class.java) - verify(tokenRepository, times(1)).delete(tokenCaptor.capture()) - assertThat(tokenCaptor.value.id).isEqualTo(42) - } - - @Test - fun shouldReturnAllTokensFromRepository() { - val expected = listOf( - Token(1, "Test Token 1", "testtoken1", "{test}hsdajfgadskjhfgsdkfjg"), - Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd") - ) - - doReturn(expected).whenever(tokenRepository).findAll() - - assertThat(tokenService.findAll()).isEqualTo(expected) - } - - @Test - fun shouldAddAllTokensFromRepositoryToUserDataManager() { - val expected = listOf( - Token(1, "Test Token 1", "testtoken1", "{test}hsdajfgadskjhfgsdkfjg"), - Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd") - ) - - doReturn(expected).whenever(tokenRepository).findAll() - - tokenService.setup() - - verify(userDetailsManager, times(expected.size)).createUser(any()) - } - -} \ No newline at end of file diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/UserRoleServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/UserRoleServiceTest.kt deleted file mode 100644 index 1cc1459..0000000 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/UserRoleServiceTest.kt +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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 . - */ - -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.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.* -import org.springframework.security.core.session.SessionInformation -import org.springframework.security.core.session.SessionRegistry -import org.springframework.security.oauth2.core.oidc.OidcIdToken -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser -import org.springframework.security.oauth2.core.oidc.user.OidcUser -import java.time.Instant -import java.util.* - -@ExtendWith(MockitoExtension::class) -class UserRoleServiceTest { - - private lateinit var userRoleRepository: UserRoleRepository - private lateinit var sessionRegistry: SessionRegistry - - private lateinit var userRoleService: UserRoleService - - @BeforeEach - fun setup( - @Mock userRoleRepository: UserRoleRepository, - @Mock sessionRegistry: SessionRegistry - ) { - this.userRoleRepository = userRoleRepository - this.sessionRegistry = sessionRegistry - - this.userRoleService = UserRoleService(userRoleRepository, sessionRegistry) - } - - @Test - fun shouldDelegateFindAllToRepository() { - userRoleService.findAll() - - verify(userRoleRepository, times(1)).findAll() - } - - @Nested - inner class WithExistingUserRole { - - @BeforeEach - fun setup() { - doAnswer { invocation -> - Optional.of( - UserRole(invocation.getArgument(0), "patrick.tester", Role.USER) - ) - }.whenever(userRoleRepository).findById(any()) - - doAnswer { _ -> - listOf( - dummyPrincipal() - ) - }.whenever(sessionRegistry).allPrincipals - } - - @Test - fun shouldUpdateUserRole() { - userRoleService.updateUserRole(1, Role.ADMIN) - - val userRoleCaptor = argumentCaptor() - verify(userRoleRepository, times(1)).save(userRoleCaptor.capture()) - - assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) - assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.ADMIN) - } - - @Test - fun shouldExpireSessionOnUpdate() { - val dummySessions = dummySessions() - whenever(sessionRegistry.getAllSessions(any(), any())).thenReturn( - dummySessions - ) - - assertThat(dummySessions.filter { it.isExpired }).hasSize(0) - - userRoleService.updateUserRole(1, Role.ADMIN) - - verify(sessionRegistry, times(1)).getAllSessions(any(), any()) - - assertThat(dummySessions.filter { it.isExpired }).hasSize(2) - } - - @Test - fun shouldDeleteUserRole() { - userRoleService.deleteUserRole(1) - - val userRoleCaptor = argumentCaptor() - verify(userRoleRepository, times(1)).delete(userRoleCaptor.capture()) - - assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) - assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.USER) - } - - @Test - fun shouldExpireSessionOnDelete() { - val dummySessions = dummySessions() - whenever(sessionRegistry.getAllSessions(any(), any())).thenReturn( - dummySessions - ) - - assertThat(dummySessions.filter { it.isExpired }).hasSize(0) - - userRoleService.deleteUserRole(1) - - verify(sessionRegistry, times(1)).getAllSessions(any(), any()) - - assertThat(dummySessions.filter { it.isExpired }).hasSize(2) - } - } - - @Nested - inner class WithoutExistingUserRole { - - @BeforeEach - fun setup() { - doAnswer { _ -> - Optional.empty() - }.whenever(userRoleRepository).findById(any()) - } - - @Test - fun shouldNotUpdateUserRole() { - userRoleService.updateUserRole(1, Role.ADMIN) - - verify(userRoleRepository, never()).save(any()) - } - - @Test - fun shouldNotExpireSessionOnUpdate() { - userRoleService.updateUserRole(1, Role.ADMIN) - - verify(sessionRegistry, never()).getAllSessions(any(), any()) - } - - @Test - fun shouldNotDeleteUserRole() { - userRoleService.deleteUserRole(1) - - verify(userRoleRepository, never()).delete(any()) - } - - @Test - fun shouldNotExpireSessionOnDelete() { - userRoleService.deleteUserRole(1) - - verify(sessionRegistry, never()).getAllSessions(any(), any()) - } - - } - - - companion object { - private fun dummyPrincipal() = DefaultOidcUser( - listOf(), - OidcIdToken( - "anytokenvalue", - Instant.now(), - Instant.now().plusSeconds(10), - mapOf("sub" to "testsub", "preferred_username" to "patrick.tester") - ) - ) - - private fun dummySessions() = listOf( - SessionInformation( - dummyPrincipal(), - "SESSIONID1", - Date.from(Instant.now()), - ), - SessionInformation( - dummyPrincipal(), - "SESSIONID2", - Date.from(Instant.now()), - ) - ) - } -} \ No newline at end of file -- cgit v1.2.3