feature/#30-group-authentication #31

Merged
william merged 10 commits from feature/#30-group-authentication into develop 2022-08-03 08:04:11 -04:00
9 changed files with 334 additions and 140 deletions
Showing only changes of commit ed0e5d89d3 - Show all commits

View File

@ -48,9 +48,7 @@ data class UserSaveDto(
val permissions: List<Permission>,
@field:JsonIgnore val isSystemUser: Boolean = false,
@field:JsonIgnore val isDefaultGroupUser: Boolean = false
@field:JsonIgnore val isSystemUser: Boolean = false
)
data class UserUpdateDto(

View File

@ -10,6 +10,7 @@ import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
import dev.fyloz.colorrecipesexplorer.service.account.GroupTokenService
import java.util.*
import javax.annotation.PostConstruct
import kotlin.collections.HashSet
interface GroupTokenLogic {
fun isDisabled(id: String): Boolean
@ -23,13 +24,15 @@ interface GroupTokenLogic {
}
@LogicComponent
class DefaultGroupTokenLogic(private val service: GroupTokenService, private val groupLogic: GroupLogic) :
class DefaultGroupTokenLogic(
private val service: GroupTokenService,
private val groupLogic: GroupLogic,
private val enabledTokensCache: HashSet<String> = hashSetOf() // In constructor for unit testing
) :
GroupTokenLogic {
private val typeName = Constants.ModelNames.GROUP_TOKEN
private val typeNameLowerCase = typeName.lowercase()
private val enabledTokensCache = hashSetOf<String>()
@PostConstruct
fun initEnabledTokensCache() {
val tokensIds = getAll().filter { it.enabled }.map { it.id.toString() }
@ -45,8 +48,10 @@ class DefaultGroupTokenLogic(private val service: GroupTokenService, private val
override fun save(dto: GroupTokenSaveDto): GroupTokenDto {
throwIfNameAlreadyExists(dto.name)
// We don't need to check for collision, because UUIDs with different names will be different
val id = generateUUIDForName(dto.name)
val token = GroupTokenDto(
generateUniqueUUIDForName(dto.name), dto.name, true, groupLogic.getById(dto.groupId)
id, dto.name, true, groupLogic.getById(dto.groupId)
)
val savedToken = service.save(token)
@ -56,11 +61,15 @@ class DefaultGroupTokenLogic(private val service: GroupTokenService, private val
}
override fun enable(id: String) = setEnabled(id, true).also {
enabledTokensCache.add(id)
if (isDisabled(id)) {
enabledTokensCache.add(id)
}
}
override fun disable(id: String) = setEnabled(id, false).also {
enabledTokensCache.remove(id)
if (!isDisabled(id)) {
enabledTokensCache.remove(id)
}
}
override fun deleteById(id: String) {
@ -72,17 +81,6 @@ class DefaultGroupTokenLogic(private val service: GroupTokenService, private val
service.save(this.copy(enabled = enabled))
}
private fun generateUniqueUUIDForName(name: String): UUID {
var id = generateUUIDForName(name)
// UUIDs do not guarantee that collisions can't happen
while (service.existsById(id)) {
id = generateUUIDForName(name)
}
return id
}
private fun generateUUIDForName(name: String) = UUID.nameUUIDFromBytes(name.toByteArray())
private fun throwIfNameAlreadyExists(name: String) {

View File

@ -15,8 +15,6 @@ import org.springframework.stereotype.Service
import java.time.Instant
import java.util.*
const val jwtClaimUser = "user"
interface JwtLogic {
/** Build a JWT for the given [userDetails]. */
fun buildUserJwt(userDetails: UserDetails): String

View File

@ -86,6 +86,6 @@ class DefaultUserService(repository: UserRepository, private val groupService: G
return perms + groupService.flattenPermissions(user.group)
}
return perms
return perms.distinctBy { it.id }
}
}

View File

@ -30,4 +30,4 @@ spring.jackson.deserialization.fail-on-null-for-primitives=true
spring.jackson.default-property-inclusion=non_null
spring.profiles.active=@spring.profiles.active@
spring.sql.init.continue-on-error=true
spring.sql.init.continue-on-error=true

View File

@ -1,100 +1,122 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
import dev.fyloz.colorrecipesexplorer.logic.account.DefaultJwtLogic
import dev.fyloz.colorrecipesexplorer.logic.account.jwtClaimUser
import dev.fyloz.colorrecipesexplorer.utils.base64encode
import dev.fyloz.colorrecipesexplorer.utils.isAround
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.jackson.io.JacksonDeserializer
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import io.mockk.clearAllMocks
import io.mockk.spyk
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import java.time.Instant
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
class DefaultJwtLogicTest {
private val objectMapper = jacksonObjectMapper()
private val securityProperties = CreSecurityProperties().apply {
jwtSecret = "XRRm7OflmFuCrOB2Xvmfsercih9DCKom"
jwtSecret = "exBwMbD9Jw7YF7HYpwXQjcsPf4SrRSSF5YTvgbj0"
jwtDuration = 1000000L
}
private val jwtParser by lazy {
Jwts.parserBuilder()
.deserializeJsonWith(JacksonDeserializer<Map<String, *>>(objectMapper))
.setSigningKey(securityProperties.jwtSecret.base64encode())
.build()
}
private val jwtService = spyk(DefaultJwtLogic(objectMapper, securityProperties))
private val jwtLogic = spyk(DefaultJwtLogic(objectMapper, securityProperties))
private val user = UserDto(0L, "Unit test", "User", "", null, listOf())
private val permissions = listOf(Permission.VIEW_RECIPES, Permission.READ_FILE, Permission.VIEW_CATALOG)
private val user = UserDto(999L, "Unit test", "User", "", null, permissions)
private val userDetails = UserDetails(user)
private val userJwt = "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiI5OTkiLCJleHAiOjE2Njk2MTc1MDcsInBlcm1zIjoiWzIsMCwzXSIsInR5cGUiOjB9.bg8hbTRsWOcx4te3L0vi8WNPXWLZO-heS7bNsO_FBpkRPy4l-MtdLOa6hx_-pXbZ"
private val groupTokenJwt = "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJhMDIyZWU3YS03NGY5LTNjYTYtYmYwZC04ZTg3OWE2NjRhOWUifQ.VaRqPJ30h8WUACPf8wVrjaxINQcc9xnbzGOcMesW_PbeN9rEGzgkgFEuV4TRGlOr"
private val groupTokenId = UUID.nameUUIDFromBytes("Unit test token".toByteArray())
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
private fun withParsedUserOutputDto(jwt: String, test: (UserDto) -> Unit) {
val serializedUser = jwtParser.parseClaimsJws(jwt)
.body.get(jwtClaimUser, String::class.java)
@Test
fun buildUserJwt_normalBehavior_buildJwtWithValidSubject() {
// Arrange
// Act
val jwt = jwtLogic.buildUserJwt(userDetails)
test(objectMapper.readValue(serializedUser))
// Assert
val parsedJwt = jwtLogic.parseUserJwt(jwt)
assertEquals(user.id.toString(), parsedJwt.id)
}
@Test
fun buildJwt_userDetails_normalBehavior_returnsJwtStringWithValidUser() {
val userDetails = UserDetails(user)
fun buildUserJwt_normalBehavior_buildJwtWithValidType() {
// Arrange
// Act
val jwt = jwtLogic.buildUserJwt(userDetails)
val builtJwt = jwtService.buildUserJwt(userDetails)
withParsedUserOutputDto(builtJwt) { parsedUser ->
assertEquals(user, parsedUser)
}
// Assert
val parsedJwt = jwtLogic.parseUserJwt(jwt)
assertFalse(parsedJwt.isGroup)
}
@Test
fun buildJwt_user_normalBehavior_returnsJwtStringWithValidUser() {
val builtJwt = jwtService.buildUserJwt(user)
fun buildUserJwt_normalBehavior_buildJwtWithValidPermissions() {
// Arrange
// Act
val jwt = jwtLogic.buildUserJwt(userDetails)
withParsedUserOutputDto(builtJwt) { parsedUser ->
assertEquals(user, parsedUser)
}
// Assert
val parsedJwt = jwtLogic.parseUserJwt(jwt)
assertEquals(userDetails.authorities, parsedJwt.authorities)
}
@Test
fun buildJwt_user_normalBehavior_returnsJwtStringWithValidSubject() {
val builtJwt = jwtService.buildUserJwt(user)
val jwtSubject = jwtParser.parseClaimsJws(builtJwt).body.subject
fun buildGroupTokenIdJwt_normalBehavior_buildJwtWithValidSubject(){
// Arrange
// Act
val jwt = jwtLogic.buildGroupTokenIdJwt(groupTokenId)
assertEquals(user.id.toString(), jwtSubject)
// Assert
val parsedGroupId = jwtLogic.parseGroupTokenIdJwt(jwt)
assertEquals(groupTokenId, parsedGroupId)
}
@Test
fun buildJwt_user_returnsJwtWithValidExpirationDate() {
val jwtExpectedExpirationDate = Instant.now().plusSeconds(securityProperties.jwtDuration)
fun parseUserJwt_normalBehavior_returnsUserWithValidId() {
// Arrange
// Act
val user = jwtLogic.parseUserJwt(userJwt)
val builtJwt = jwtService.buildUserJwt(user)
val jwtExpiration = jwtParser.parseClaimsJws(builtJwt)
.body.expiration.toInstant()
// Check if it's between 1 second
assertTrue { jwtExpiration.isAround(jwtExpectedExpirationDate) }
// Assert
assertEquals(userDetails.id, user.id)
}
// parseJwt()
@Test
fun parseUserJwt_normalBehavior_returnsUserWithValidType() {
// Arrange
// Act
val user = jwtLogic.parseUserJwt(userJwt)
// Assert
assertFalse(user.isGroup)
}
@Test
fun parseJwt_normalBehavior_returnsExpectedUser() {
val jwt = jwtService.buildUserJwt(user)
val parsedUser = jwtService.parseUserJwt(jwt)
fun parseUserJwt_normalBehavior_returnsUserWithValidPermissions() {
// Arrange
// Act
val user = jwtLogic.parseUserJwt(userJwt)
assertEquals(user, parsedUser)
// Assert
assertEquals(userDetails.authorities, user.authorities)
}
@Test
fun parseGroupTokenId_normalBehavior_returnsValidGroupTokenId() {
// Arrange
// Act
val parsedGroupTokenId = jwtLogic.parseGroupTokenIdJwt(groupTokenJwt)
// Assert
assertEquals(groupTokenId, parsedGroupTokenId)
}
}

View File

@ -23,8 +23,7 @@ class DefaultGroupLogicTest {
}
private val userLogicMock = mockk<UserLogic> {
every { getAllByGroup(any()) } returns listOf()
every { getById(any(), any(), any()) } returns user
every { getDefaultGroupUser(any()) } returns user
every { getById(any(), any()) } returns user
every { deleteById(any()) } just runs
}
@ -69,16 +68,4 @@ class DefaultGroupLogicTest {
// Assert
assertThrows<AlreadyExistsException> { groupLogic.update(group) }
}
@Test
fun deleteById_normalBehavior_callsDeleteByIdInUserLogicWithDefaultGroupUserId() {
// Arrange
// Act
groupLogic.deleteById(group.id)
// Assert
verify {
userLogicMock.deleteById(group.defaultGroupUserId)
}
}
}

View File

@ -0,0 +1,234 @@
package dev.fyloz.colorrecipesexplorer.logic.account
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupTokenDto
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupTokenSaveDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.service.account.GroupTokenService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.util.*
import kotlin.test.*
class DefaultGroupTokenLogicTest {
private val groupTokenServiceMock = mockk<GroupTokenService>()
private val groupLogicMock = mockk<GroupLogic>()
private val enabledTokenCache = hashSetOf<String>()
private val groupTokenLogic = spyk(DefaultGroupTokenLogic(groupTokenServiceMock, groupLogicMock, enabledTokenCache))
private val groupTokenName = "Unit test token"
private val groupTokenId = UUID.nameUUIDFromBytes(groupTokenName.toByteArray())
private val groupTokenIdStr = groupTokenId.toString()
private val group = GroupDto(1L, "Unit test group", listOf(), listOf())
private val groupToken = GroupTokenDto(groupTokenId, groupTokenName, true, group)
private val groupTokenSaveDto = GroupTokenSaveDto(groupTokenName, group.id)
@AfterEach
fun afterEach() {
clearAllMocks()
enabledTokenCache.clear()
}
@Test
fun isDisabled_groupTokenIdInCache_returnsFalse() {
// Arrange
enabledTokenCache.add(groupTokenIdStr)
// Act
val disabled = groupTokenLogic.isDisabled(groupTokenIdStr)
// Assert
assertFalse(disabled)
}
@Test
fun isDisabled_groupTokenIdNotInCache_returnsTrue() {
// Arrange
// Act
val disabled = groupTokenLogic.isDisabled(groupTokenIdStr)
// Assert
assertTrue(disabled)
}
@Test
fun getAll_normalBehavior_returnsFromService() {
// Arrange
val expectedGroupTokens = listOf(groupToken)
every { groupTokenServiceMock.getAll() } returns expectedGroupTokens
// Act
val actualGroupTokens = groupTokenLogic.getAll()
// Assert
assertEquals(expectedGroupTokens, actualGroupTokens)
}
@Test
fun getById_string_normalBehavior_callsGetByIdWithValidUUID() {
// Arrange
every { groupTokenLogic.getById(any<UUID>()) } returns groupToken
// Act
groupTokenLogic.getById(groupTokenIdStr)
// Assert
verify {
groupTokenLogic.getById(groupTokenId)
}
}
@Test
fun getById_uuid_normalBehavior_returnsFromService() {
// Arrange
every { groupTokenServiceMock.getById(any()) } returns groupToken
// Act
val actualGroupToken = groupTokenLogic.getById(groupTokenId)
// Assert
assertSame(groupToken, actualGroupToken)
}
@Test
fun getById_uuid_notFound_throwsNotFoundException() {
// Arrange
every { groupTokenServiceMock.getById(any()) } returns null
// Act
// Assert
assertThrows<NotFoundException> { groupTokenLogic.getById(groupTokenId) }
}
@Test
fun save_normalBehavior_callsSaveInService() {
// Arrange
every { groupTokenServiceMock.existsByName(any()) } returns false
every { groupTokenServiceMock.save(any()) } returns groupToken
every { groupLogicMock.getById(any()) } returns group
// Act
groupTokenLogic.save(groupTokenSaveDto)
// Assert
verify {
groupTokenServiceMock.save(groupToken)
}
}
@Test
fun save_normalBehavior_addsIdToEnabledTokensCache() {
// Arrange
every { groupTokenServiceMock.existsByName(any()) } returns false
every { groupTokenServiceMock.save(any()) } returns groupToken
every { groupLogicMock.getById(any()) } returns group
// Act
groupTokenLogic.save(groupTokenSaveDto)
// Assert
assertContains(enabledTokenCache, groupTokenIdStr)
}
@Test
fun save_nameAlreadyExists_throwsAlreadyExistsException() {
// Arrange
every { groupTokenServiceMock.existsByName(any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { groupTokenLogic.save(groupTokenSaveDto) }
}
@Test
fun enable_normalBehavior_savesTokenInService() {
// Arrange
every { groupTokenServiceMock.save(any()) } returns groupToken
every { groupTokenLogic.getById(any<String>()) } returns groupToken
// Act
groupTokenLogic.enable(groupTokenIdStr)
// Assert
verify {
groupTokenServiceMock.save(match {
it.id == groupTokenId && it.name == groupTokenName && it.enabled
})
}
}
@Test
fun enable_normalBehavior_addsIdToEnabledTokensCache() {
// Arrange
every { groupTokenServiceMock.save(any()) } returns groupToken
every { groupTokenLogic.getById(any<String>()) } returns groupToken
// Act
groupTokenLogic.enable(groupTokenIdStr)
// Assert
assertContains(enabledTokenCache, groupTokenIdStr)
}
@Test
fun disable_normalBehavior_savesTokenInService() {
// Arrange
every { groupTokenServiceMock.save(any()) } returns groupToken
every { groupTokenLogic.getById(any<String>()) } returns groupToken
// Act
groupTokenLogic.disable(groupTokenIdStr)
// Assert
verify {
groupTokenServiceMock.save(match {
it.id == groupTokenId && it.name == groupTokenName && !it.enabled
})
}
}
@Test
fun disable_normalBehavior_removesIdFromEnabledTokensCache() {
// Arrange
every { groupTokenServiceMock.save(any()) } returns groupToken
every { groupTokenLogic.getById(any<String>()) } returns groupToken
// Act
groupTokenLogic.disable(groupTokenIdStr)
// Assert
assertFalse(enabledTokenCache.contains(groupTokenIdStr))
}
@Test
fun deleteById_normalBehavior_callsService() {
// Arrange
every { groupTokenServiceMock.deleteById(any()) } just runs
// Act
groupTokenLogic.deleteById(groupTokenIdStr)
// Assert
verify {
groupTokenServiceMock.deleteById(groupTokenId)
}
}
@Test
fun deleteById_normalBehavior_removesIdFromEnabledTokensCache() {
// Arrange
every { groupTokenServiceMock.deleteById(any()) } just runs
// Act
groupTokenLogic.deleteById(groupTokenIdStr)
// Assert
assertFalse(enabledTokenCache.contains(groupTokenIdStr))
}
}

View File

@ -22,11 +22,10 @@ class DefaultUserLogicTest {
private val userServiceMock = mockk<UserService> {
every { existsById(any()) } returns false
every { existsByFirstNameAndLastName(any(), any(), any()) } returns false
every { getAll(any(), any()) } returns listOf()
every { getAll(any()) } returns listOf()
every { getAllByGroup(any()) } returns listOf()
every { getById(any(), any(), any()) } returns user
every { getById(any(), any()) } returns user
every { getByFirstNameAndLastName(any(), any()) } returns user
every { getDefaultGroupUser(any()) } returns user
}
private val groupLogicMock = mockk<GroupLogic> {
every { getById(any()) } returns group
@ -44,8 +43,7 @@ class DefaultUserLogicTest {
user.password,
null,
user.permissions,
user.isSystemUser,
user.isDefaultGroupUser
user.isSystemUser
)
private val userUpdateDto = UserUpdateDto(user.id, user.firstName, user.lastName, null, listOf())
@ -62,7 +60,7 @@ class DefaultUserLogicTest {
// Assert
verify {
userServiceMock.getAll(isSystemUser = false, isDefaultGroupUser = false)
userServiceMock.getAll(isSystemUser = false)
}
confirmVerified(userServiceMock)
}
@ -88,7 +86,7 @@ class DefaultUserLogicTest {
// Assert
verify {
userLogic.getById(user.id, isSystemUser = false, isDefaultGroupUser = false)
userLogic.getById(user.id, isSystemUser = false)
}
}
@ -96,11 +94,11 @@ class DefaultUserLogicTest {
fun getById_normalBehavior_callsGetByIdInService() {
// Arrange
// Act
userLogic.getById(user.id, isSystemUser = false, isDefaultGroupUser = true)
userLogic.getById(user.id, isSystemUser = false)
// Assert
verify {
userServiceMock.getById(user.id, isSystemUser = false, isDefaultGroupUser = true)
userServiceMock.getById(user.id, isSystemUser = false)
}
confirmVerified(userServiceMock)
}
@ -108,54 +106,13 @@ class DefaultUserLogicTest {
@Test
fun getById_notFound_throwsNotFoundException() {
// Arrange
every { userServiceMock.getById(any(), any(), any()) } returns null
every { userServiceMock.getById(any(), any()) } returns null
// Act
// Assert
assertThrows<NotFoundException> { userLogic.getById(user.id) }
}
@Test
fun getDefaultGroupUser_normalBehavior_callsGetDefaultGroupUserInService() {
// Arrange
// Act
userLogic.getDefaultGroupUser(group)
// Assert
verify {
userServiceMock.getDefaultGroupUser(group)
}
confirmVerified(userServiceMock)
}
@Test
fun getDefaultGroupUser_notFound_throwsNotFoundException() {
// Arrange
every { userServiceMock.getDefaultGroupUser(any()) } returns null
// Act
// Assert
assertThrows<NotFoundException> { userLogic.getDefaultGroupUser(group) }
}
@Test
fun saveDefaultGroupUser_normalBehavior_callsSaveWithValidSaveDto() {
// Arrange
every { userLogic.save(any<UserSaveDto>()) } returns user
val expectedSaveDto = UserSaveDto(
group.defaultGroupUserId, group.name, "User", group.name, group.id, listOf(), isDefaultGroupUser = true
)
// Act
userLogic.saveDefaultGroupUser(group)
// Assert
verify {
userLogic.save(expectedSaveDto)
}
}
@Test
fun save_dto_normalBehavior_callsSaveWithValidUser() {
// Arrange
@ -208,7 +165,7 @@ class DefaultUserLogicTest {
@Test
fun update_dto_normalBehavior_callsUpdateWithValidUser() {
// Arrange
every { userLogic.getById(any(), any(), any()) } returns user
every { userLogic.getById(any(), any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
// Act