Merge pull request 'feature/#30-group-authentication' (#31) from feature/#30-group-authentication into develop
Reviewed-on: #31
This commit is contained in:
commit
3e6b36ea17
|
@ -106,10 +106,6 @@ tasks.withType<JavaCompile>() {
|
|||
tasks.withType<KotlinCompile>().all {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-Xinline-classes"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,33 @@
|
|||
package dev.fyloz.colorrecipesexplorer
|
||||
|
||||
object Constants {
|
||||
val BEARER_PREFIX = "Bearer"
|
||||
var DEBUG_MODE = false // Not really a constant, but should never change after the app startup
|
||||
|
||||
object ControllerPaths {
|
||||
const val COMPANY = "/api/company"
|
||||
const val FILE = "/api/file"
|
||||
const val GROUP = "/api/user/group"
|
||||
const val INVENTORY = "/api/inventory"
|
||||
const val MATERIAL = "/api/material"
|
||||
const val MATERIAL_TYPE = "/api/materialtype"
|
||||
const val MIX = "/api/recipe/mix"
|
||||
const val RECIPE = "/api/recipe"
|
||||
const val TOUCH_UP_KIT = "/api/touchupkit"
|
||||
const val USER = "/api/user"
|
||||
const val BASE_PATH = "/api"
|
||||
const val ACCOUNT_BASE_PATH = "$BASE_PATH/account"
|
||||
|
||||
const val COMPANY = "$BASE_PATH/company"
|
||||
const val GROUP_TOKEN = "$BASE_PATH/account/group/token"
|
||||
const val FILE = "$BASE_PATH/file"
|
||||
const val INVENTORY = "$BASE_PATH/inventory"
|
||||
const val MATERIAL = "$BASE_PATH/material"
|
||||
const val MATERIAL_TYPE = "$BASE_PATH/materialtype"
|
||||
const val MIX = "$BASE_PATH/recipe/mix"
|
||||
const val RECIPE = "$BASE_PATH/recipe"
|
||||
const val TOUCH_UP_KIT = "$BASE_PATH/touchupkit"
|
||||
|
||||
const val GROUP = "$ACCOUNT_BASE_PATH/group"
|
||||
const val GROUP_LOGIN = "$ACCOUNT_BASE_PATH/login/group"
|
||||
const val LOGIN = "$ACCOUNT_BASE_PATH/login"
|
||||
const val LOGOUT = "$ACCOUNT_BASE_PATH/logout"
|
||||
const val USER = "$ACCOUNT_BASE_PATH/user"
|
||||
}
|
||||
|
||||
object CookieNames {
|
||||
const val AUTHORIZATION = "Authorization"
|
||||
const val GROUP_TOKEN = "Group-Token"
|
||||
}
|
||||
|
||||
object FilePaths {
|
||||
|
@ -23,8 +39,14 @@ object Constants {
|
|||
const val RECIPE_IMAGES = "$IMAGES/recipes"
|
||||
}
|
||||
|
||||
object JwtType {
|
||||
const val USER = 0
|
||||
const val GROUP = 1
|
||||
}
|
||||
|
||||
object ModelNames {
|
||||
const val COMPANY = "Company"
|
||||
const val GROUP_TOKEN = "GroupToken"
|
||||
const val GROUP = "Group"
|
||||
const val MATERIAL = "Material"
|
||||
const val MATERIAL_TYPE = "MaterialType"
|
||||
|
@ -47,4 +69,4 @@ object Constants {
|
|||
object ValidationRegexes {
|
||||
const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,12 @@ package dev.fyloz.colorrecipesexplorer.config.annotations
|
|||
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('ADMIN')")
|
||||
annotation class PreAuthorizeAdmin
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import java.util.*
|
||||
|
||||
class GroupAuthenticationToken(val id: UUID) : AbstractAuthenticationToken(null) {
|
||||
override fun getPrincipal() = id
|
||||
|
||||
// There is no credential needed to log in with a group token, just use the group token id
|
||||
override fun getCredentials() = id
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupTokenDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupTokenLogic
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import java.util.*
|
||||
|
||||
class GroupTokenAuthenticationProvider(private val groupTokenLogic: GroupTokenLogic) : AuthenticationProvider {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun authenticate(authentication: Authentication): Authentication {
|
||||
val groupAuthenticationToken = authentication as GroupAuthenticationToken
|
||||
|
||||
val groupToken = try {
|
||||
retrieveGroupToken(groupAuthenticationToken.id)
|
||||
} catch (e: AuthenticationException) {
|
||||
logger.debug(e.message)
|
||||
throw e
|
||||
}
|
||||
|
||||
val userDetails =
|
||||
UserDetails(
|
||||
groupToken.id.toString(),
|
||||
groupToken.name,
|
||||
"",
|
||||
groupToken.group,
|
||||
groupToken.group.permissions,
|
||||
true
|
||||
)
|
||||
return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
|
||||
}
|
||||
|
||||
override fun supports(authentication: Class<*>) =
|
||||
authentication.isAssignableFrom(GroupAuthenticationToken::class.java)
|
||||
|
||||
private fun retrieveGroupToken(id: UUID): GroupTokenDto {
|
||||
val groupToken = try {
|
||||
groupTokenLogic.getById(id)
|
||||
} catch (_: NotFoundException) {
|
||||
throw BadCredentialsException("Failed to find group token with id '$id'")
|
||||
}
|
||||
|
||||
if (groupTokenLogic.isDisabled(groupToken.id.toString())) {
|
||||
throw BadCredentialsException("Group token '${groupToken.id}' is disabled")
|
||||
}
|
||||
|
||||
return groupToken
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserLoginRequestDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
|
||||
import dev.fyloz.colorrecipesexplorer.utils.addCookie
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.web.util.WebUtils
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
const val authorizationCookieName = "Authorization"
|
||||
const val defaultGroupCookieName = "Default-Group"
|
||||
val blacklistedJwtTokens = mutableListOf<String>() // Not working, move to a cache or something
|
||||
|
||||
class JwtAuthenticationFilter(
|
||||
private val authManager: AuthenticationManager,
|
||||
private val jwtLogic: JwtLogic,
|
||||
private val securityProperties: CreSecurityProperties,
|
||||
private val updateUserLoginTime: (Long) -> Unit
|
||||
) : UsernamePasswordAuthenticationFilter() {
|
||||
private var debugMode = false
|
||||
|
||||
init {
|
||||
setFilterProcessesUrl("/api/login")
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
}
|
||||
|
||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequestDto::class.java)
|
||||
logger.debug("Login attempt for user ${loginRequest.id}...")
|
||||
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
|
||||
}
|
||||
|
||||
override fun successfulAuthentication(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
chain: FilterChain,
|
||||
auth: Authentication
|
||||
) {
|
||||
val userDetails = auth.principal as UserDetails
|
||||
val token = jwtLogic.buildJwt(userDetails)
|
||||
|
||||
with(userDetails.user) {
|
||||
logger.info("User ${this.id} (${this.firstName} ${this.lastName}) has logged in successfully")
|
||||
}
|
||||
|
||||
response.addHeader("Access-Control-Expose-Headers", authorizationCookieName)
|
||||
response.addHeader(authorizationCookieName, "Bearer $token")
|
||||
response.addCookie(authorizationCookieName, "Bearer$token") {
|
||||
httpOnly = true
|
||||
sameSite = true
|
||||
secure = !debugMode
|
||||
maxAge = securityProperties.jwtDuration / 1000
|
||||
}
|
||||
|
||||
updateUserLoginTime(userDetails.user.id)
|
||||
}
|
||||
}
|
||||
|
||||
class JwtAuthorizationFilter(
|
||||
private val jwtLogic: JwtLogic,
|
||||
authenticationManager: AuthenticationManager,
|
||||
private val userDetailsLogic: UserDetailsLogic
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
fun tryLoginFromBearer(): Boolean {
|
||||
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
|
||||
// Check for an authorization token cookie or header
|
||||
val authorizationToken = if (authorizationCookie != null)
|
||||
authorizationCookie.value
|
||||
else
|
||||
request.getHeader(authorizationCookieName)
|
||||
|
||||
// An authorization token is valid if it starts with "Bearer", is not expired and is not blacklisted
|
||||
if (authorizationToken != null && authorizationToken.startsWith("Bearer") && authorizationToken !in blacklistedJwtTokens) {
|
||||
val authenticationToken = getAuthentication(authorizationToken) ?: return false
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun tryLoginFromDefaultGroupCookie() {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
if (defaultGroupCookie != null) {
|
||||
val authenticationToken = getAuthenticationToken(defaultGroupCookie.value)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
}
|
||||
}
|
||||
|
||||
if (!tryLoginFromBearer())
|
||||
tryLoginFromDefaultGroupCookie()
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
|
||||
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
|
||||
return try {
|
||||
val user = jwtLogic.parseJwt(token.replace("Bearer", ""))
|
||||
getAuthenticationToken(user)
|
||||
} catch (_: ExpiredJwtException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthenticationToken(user: UserDto) =
|
||||
UsernamePasswordAuthenticationToken(user.id, null, user.authorities)
|
||||
|
||||
private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try {
|
||||
val userDetails = userDetailsLogic.loadUserById(userId)
|
||||
UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
|
||||
} catch (_: NotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun getAuthenticationToken(userId: String) =
|
||||
getAuthenticationToken(userId.toLong())
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.filters.GroupTokenAuthenticationFilter
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.filters.JwtAuthorizationFilter
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.filters.UsernamePasswordAuthenticationFilter
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupTokenLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.UserDetailsLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.Logger
|
||||
|
@ -25,6 +30,7 @@ import org.springframework.security.config.http.SessionCreationPolicy
|
|||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
|
@ -39,13 +45,13 @@ private const val rootUserLastName = "User"
|
|||
abstract class BaseSecurityConfig(
|
||||
private val userDetailsLogic: UserDetailsLogic,
|
||||
private val jwtLogic: JwtLogic,
|
||||
private val groupTokenLogic: GroupTokenLogic,
|
||||
private val environment: Environment,
|
||||
protected val securityProperties: CreSecurityProperties
|
||||
) : WebSecurityConfigurerAdapter() {
|
||||
protected abstract val logger: Logger
|
||||
|
||||
protected val passwordEncoder = BCryptPasswordEncoder()
|
||||
var debugMode = false
|
||||
|
||||
@Bean
|
||||
open fun passwordEncoder() =
|
||||
|
@ -69,33 +75,43 @@ abstract class BaseSecurityConfig(
|
|||
}
|
||||
|
||||
override fun configure(authBuilder: AuthenticationManagerBuilder) {
|
||||
authBuilder.userDetailsService(userDetailsLogic).passwordEncoder(passwordEncoder)
|
||||
authBuilder
|
||||
.authenticationProvider(GroupTokenAuthenticationProvider(groupTokenLogic))
|
||||
.userDetailsService(userDetailsLogic).passwordEncoder(passwordEncoder)
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
val authManager = authenticationManager()
|
||||
|
||||
http
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.addFilter(
|
||||
JwtAuthenticationFilter(
|
||||
authenticationManager(),
|
||||
.addFilterBefore(
|
||||
GroupTokenAuthenticationFilter(jwtLogic, securityProperties, groupTokenLogic, authManager),
|
||||
BasicAuthenticationFilter::class.java
|
||||
)
|
||||
.addFilterBefore(
|
||||
UsernamePasswordAuthenticationFilter(
|
||||
jwtLogic,
|
||||
securityProperties,
|
||||
authManager,
|
||||
this::updateUserLoginTime
|
||||
)
|
||||
),
|
||||
BasicAuthenticationFilter::class.java
|
||||
)
|
||||
.addFilter(
|
||||
JwtAuthorizationFilter(jwtLogic, authenticationManager(), userDetailsLogic)
|
||||
JwtAuthorizationFilter(jwtLogic, groupTokenLogic, authManager)
|
||||
)
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/api/config/**").permitAll() // Allow access to logo and icon
|
||||
.antMatchers("/api/login").permitAll() // Allow access to login
|
||||
.antMatchers("/api/account/login").permitAll() // Allow access to login
|
||||
.antMatchers("/api/account/login/group").permitAll() // Allow access to group login
|
||||
.antMatchers("**").fullyAuthenticated()
|
||||
|
||||
if (debugMode) {
|
||||
if (Constants.DEBUG_MODE) {
|
||||
http
|
||||
.cors()
|
||||
}
|
||||
|
@ -103,8 +119,10 @@ abstract class BaseSecurityConfig(
|
|||
|
||||
@PostConstruct
|
||||
fun initDebugMode() {
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
val debugMode = "debug" in environment.activeProfiles
|
||||
if (debugMode) logger.warn("Debug mode is enabled, security will be decreased!")
|
||||
|
||||
Constants.DEBUG_MODE = debugMode
|
||||
}
|
||||
|
||||
protected open fun updateUserLoginTime(userId: Long) {
|
||||
|
@ -120,9 +138,10 @@ class SecurityConfig(
|
|||
@Lazy userDetailsLogic: UserDetailsLogic,
|
||||
@Lazy private val userLogic: UserLogic,
|
||||
jwtLogic: JwtLogic,
|
||||
groupTokenLogic: GroupTokenLogic,
|
||||
environment: Environment,
|
||||
securityProperties: CreSecurityProperties
|
||||
) : BaseSecurityConfig(userDetailsLogic, jwtLogic, environment, securityProperties) {
|
||||
) : BaseSecurityConfig(userDetailsLogic, jwtLogic, groupTokenLogic, environment, securityProperties) {
|
||||
override val logger = KotlinLogging.logger {}
|
||||
|
||||
@PostConstruct
|
||||
|
@ -168,9 +187,10 @@ class SecurityConfig(
|
|||
class EmergencySecurityConfig(
|
||||
userDetailsLogic: UserDetailsLogic,
|
||||
jwtLogic: JwtLogic,
|
||||
groupTokenLogic: GroupTokenLogic,
|
||||
environment: Environment,
|
||||
securityProperties: CreSecurityProperties
|
||||
) : BaseSecurityConfig(userDetailsLogic, jwtLogic, environment, securityProperties) {
|
||||
) : BaseSecurityConfig(userDetailsLogic, jwtLogic, groupTokenLogic, environment, securityProperties) {
|
||||
override val logger = KotlinLogging.logger {}
|
||||
|
||||
init {
|
||||
|
@ -187,5 +207,5 @@ class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
|
|||
) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
|
||||
}
|
||||
|
||||
private class InvalidSystemUserException(userType: String, message: String) :
|
||||
class InvalidSystemUserException(userType: String, message: String) :
|
||||
RuntimeException("Invalid $userType user: $message")
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security.filters
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.GroupAuthenticationToken
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupTokenLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
import org.springframework.security.core.Authentication
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class GroupTokenAuthenticationFilter(
|
||||
jwtLogic: JwtLogic,
|
||||
securityProperties: CreSecurityProperties,
|
||||
private val groupTokenLogic: GroupTokenLogic,
|
||||
private val authManager: AuthenticationManager
|
||||
) : JwtAuthenticationFilter(Constants.ControllerPaths.GROUP_LOGIN, securityProperties, jwtLogic) {
|
||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||
val groupTokenId = groupTokenLogic.getIdForRequest(request)
|
||||
?: throw BadCredentialsException("Required group token cookie was not present")
|
||||
|
||||
logger.debug("Login attempt for group token $groupTokenId")
|
||||
return authManager.authenticate(GroupAuthenticationToken(groupTokenId))
|
||||
}
|
||||
|
||||
override fun afterSuccessfulAuthentication(userDetails: UserDetails) {
|
||||
logger.info("Successful login for group id '${userDetails.group!!.id}' using token '${userDetails.id}' (${userDetails.username})")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security.filters
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserLoginResponse
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.utils.addCookie
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
abstract class JwtAuthenticationFilter(
|
||||
filterProcessesUrl: String,
|
||||
private val securityProperties: CreSecurityProperties,
|
||||
private val jwtLogic: JwtLogic
|
||||
) :
|
||||
AbstractAuthenticationProcessingFilter(
|
||||
AntPathRequestMatcher(filterProcessesUrl, HttpMethod.POST.toString())
|
||||
) {
|
||||
private val jacksonObjectMapper = jacksonObjectMapper()
|
||||
|
||||
override fun successfulAuthentication(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
chain: FilterChain,
|
||||
auth: Authentication
|
||||
) {
|
||||
val userDetails = auth.principal as UserDetails
|
||||
val token = jwtLogic.buildUserJwt(userDetails)
|
||||
|
||||
addAuthorizationCookie(response, token)
|
||||
addResponseBody(userDetails, response)
|
||||
|
||||
afterSuccessfulAuthentication(userDetails)
|
||||
}
|
||||
|
||||
protected abstract fun afterSuccessfulAuthentication(userDetails: UserDetails)
|
||||
|
||||
private fun addAuthorizationCookie(response: HttpServletResponse, token: String) {
|
||||
response.addCookie(Constants.CookieNames.AUTHORIZATION, Constants.BEARER_PREFIX + token) {
|
||||
httpOnly = AUTHORIZATION_COOKIE_HTTP_ONLY
|
||||
sameSite = AUTHORIZATION_COOKIE_SAME_SITE
|
||||
secure = !Constants.DEBUG_MODE
|
||||
maxAge = securityProperties.jwtDuration / 1000
|
||||
path = AUTHORIZATION_COOKIE_PATH
|
||||
}
|
||||
}
|
||||
|
||||
private fun addResponseBody(userDetails: UserDetails, response: HttpServletResponse) {
|
||||
val body = getResponseBody(userDetails)
|
||||
val serializedBody = jacksonObjectMapper.writeValueAsString(body)
|
||||
|
||||
response.writer.println(serializedBody)
|
||||
}
|
||||
|
||||
private fun getResponseBody(userDetails: UserDetails) =
|
||||
UserLoginResponse(
|
||||
userDetails.id,
|
||||
userDetails.username,
|
||||
userDetails.group?.id,
|
||||
userDetails.group?.name,
|
||||
userDetails.permissions
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val AUTHORIZATION_COOKIE_HTTP_ONLY = true
|
||||
private const val AUTHORIZATION_COOKIE_SAME_SITE = true
|
||||
private const val AUTHORIZATION_COOKIE_PATH = Constants.ControllerPaths.BASE_PATH
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security.filters
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserJwt
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupTokenLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.utils.parseBearer
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.web.util.WebUtils
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class JwtAuthorizationFilter(
|
||||
private val jwtLogic: JwtLogic,
|
||||
private val groupTokenLogic: GroupTokenLogic,
|
||||
authenticationManager: AuthenticationManager
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
val authorizationCookie = WebUtils.getCookie(request, Constants.CookieNames.AUTHORIZATION)
|
||||
|
||||
// If there is no authorization cookie, the user is not authenticated
|
||||
if (authorizationCookie == null) {
|
||||
chain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
val authorizationToken = authorizationCookie.value
|
||||
if (!isJwtValid(authorizationToken)) {
|
||||
logger.debug("Received request with invalid ${Constants.CookieNames.AUTHORIZATION} cookie")
|
||||
|
||||
chain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
SecurityContextHolder.getContext().authentication = getAuthentication(authorizationToken)
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
|
||||
// The authorization token is valid if it starts with "Bearer"
|
||||
private fun isJwtValid(authorizationToken: String) =
|
||||
authorizationToken.startsWith(Constants.BEARER_PREFIX)
|
||||
|
||||
private fun getAuthentication(authorizationToken: String): Authentication? {
|
||||
return try {
|
||||
val jwt = parseBearer(authorizationToken)
|
||||
val user = jwtLogic.parseUserJwt(jwt)
|
||||
|
||||
if (user.isGroup && groupTokenLogic.isDisabled(user.id)) {
|
||||
logger.debug("Rejected authorization for disabled group token '${user.id}'")
|
||||
return null
|
||||
}
|
||||
|
||||
getAuthentication(user)
|
||||
} catch (_: ExpiredJwtException) {
|
||||
logger.debug("Rejected authorization for expired JWT")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthentication(user: UserJwt) =
|
||||
UsernamePasswordAuthenticationToken(user.id, null, user.authorities)
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security.filters
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserLoginRequestDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
val blacklistedJwtTokens = mutableListOf<String>()
|
||||
|
||||
class UsernamePasswordAuthenticationFilter(
|
||||
jwtLogic: JwtLogic,
|
||||
securityProperties: CreSecurityProperties,
|
||||
private val authManager: AuthenticationManager,
|
||||
private val updateUserLoginTime: (Long) -> Unit
|
||||
) : JwtAuthenticationFilter(Constants.ControllerPaths.LOGIN, securityProperties, jwtLogic) {
|
||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||
val loginRequest = getLoginRequest(request)
|
||||
val authenticationToken = UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password)
|
||||
|
||||
logger.debug("Login attempt for user ${loginRequest.id}")
|
||||
return authManager.authenticate(authenticationToken)
|
||||
}
|
||||
|
||||
override fun afterSuccessfulAuthentication(userDetails: UserDetails) {
|
||||
updateUserLoginTime(userDetails.id.toLong())
|
||||
logger.info("User ${userDetails.id} (${userDetails.username}) has logged in successfully")
|
||||
}
|
||||
|
||||
private fun getLoginRequest(request: HttpServletRequest) =
|
||||
jacksonObjectMapper().readValue(request.inputStream, UserLoginRequestDto::class.java)
|
||||
}
|
|
@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.dtos
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import java.time.LocalDate
|
||||
import javax.validation.constraints.Max
|
||||
import javax.validation.constraints.Min
|
||||
|
@ -118,4 +119,4 @@ data class RecipePublicDataDto(
|
|||
val notes: List<RecipeGroupNoteDto>,
|
||||
|
||||
val mixesLocation: List<MixLocationDto>
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
|
||||
import java.time.LocalDateTime
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
data class UserDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val firstName: String,
|
||||
|
||||
val lastName: String,
|
||||
|
||||
@field:JsonIgnore
|
||||
val password: String = "",
|
||||
|
||||
val group: GroupDto?,
|
||||
|
||||
val permissions: List<Permission>,
|
||||
|
||||
val explicitPermissions: List<Permission> = listOf(),
|
||||
|
||||
val lastLoginTime: LocalDateTime? = null,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isDefaultGroupUser: Boolean = false,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isSystemUser: Boolean = false
|
||||
) : EntityDto {
|
||||
@get:JsonIgnore
|
||||
val authorities
|
||||
get() = permissions
|
||||
.map { it.toAuthority() }
|
||||
.toMutableSet()
|
||||
}
|
||||
|
||||
data class UserSaveDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Size(min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL)
|
||||
val password: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
val permissions: List<Permission>,
|
||||
|
||||
// TODO WN: Test if working
|
||||
// @JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@field:JsonIgnore
|
||||
val isSystemUser: Boolean = false,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isDefaultGroupUser: Boolean = false
|
||||
)
|
||||
|
||||
data class UserUpdateDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
val permissions: List<Permission>
|
||||
)
|
||||
|
||||
data class UserLoginRequestDto(val id: Long, val password: String)
|
||||
|
||||
class UserDetails(val user: UserDto) : SpringUserDetails {
|
||||
override fun getPassword() = user.password
|
||||
override fun getUsername() = user.id.toString()
|
||||
override fun getAuthorities() = user.authorities
|
||||
|
||||
override fun isAccountNonExpired() = true
|
||||
override fun isAccountNonLocked() = true
|
||||
override fun isCredentialsNonExpired() = true
|
||||
override fun isEnabled() = true
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
package dev.fyloz.colorrecipesexplorer.dtos.account
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotEmpty
|
||||
|
@ -15,11 +16,4 @@ data class GroupDto(
|
|||
val permissions: List<Permission>,
|
||||
|
||||
val explicitPermissions: List<Permission> = listOf()
|
||||
) : EntityDto {
|
||||
@get:JsonIgnore
|
||||
val defaultGroupUserId = getDefaultGroupUserId(id)
|
||||
|
||||
companion object {
|
||||
fun getDefaultGroupUserId(id: Long) = 1000000 + id
|
||||
}
|
||||
}
|
||||
) : EntityDto
|
|
@ -0,0 +1,23 @@
|
|||
package dev.fyloz.colorrecipesexplorer.dtos.account
|
||||
|
||||
import java.util.UUID
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class GroupTokenDto(
|
||||
val id: UUID,
|
||||
|
||||
val name: String,
|
||||
|
||||
val enabled: Boolean,
|
||||
|
||||
val isDeleted: Boolean,
|
||||
|
||||
val group: GroupDto
|
||||
)
|
||||
|
||||
data class GroupTokenSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
val groupId: Long
|
||||
)
|
|
@ -0,0 +1,98 @@
|
|||
package dev.fyloz.colorrecipesexplorer.dtos.account
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import java.time.LocalDateTime
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
data class UserDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val firstName: String,
|
||||
|
||||
val lastName: String,
|
||||
|
||||
@field:JsonIgnore val password: String = "",
|
||||
|
||||
val group: GroupDto?,
|
||||
|
||||
val permissions: List<Permission>,
|
||||
|
||||
val explicitPermissions: List<Permission> = listOf(),
|
||||
|
||||
val lastLoginTime: LocalDateTime? = null,
|
||||
|
||||
@field:JsonIgnore val isSystemUser: Boolean = false
|
||||
) : EntityDto {
|
||||
@get:JsonIgnore
|
||||
val fullName = "$firstName $lastName"
|
||||
}
|
||||
|
||||
data class UserSaveDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank val firstName: String,
|
||||
|
||||
@field:NotBlank val lastName: String,
|
||||
|
||||
@field:NotBlank @field:Size(
|
||||
min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL
|
||||
) val password: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
val permissions: List<Permission>,
|
||||
|
||||
@field:JsonIgnore val isSystemUser: Boolean = false
|
||||
)
|
||||
|
||||
data class UserUpdateDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank val firstName: String,
|
||||
|
||||
@field:NotBlank val lastName: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
val permissions: List<Permission>
|
||||
)
|
||||
|
||||
data class UserLoginRequestDto(val id: Long, val password: String)
|
||||
|
||||
data class UserJwt(val id: String, val authorities: Collection<GrantedAuthority>, val isGroup: Boolean)
|
||||
|
||||
class UserDetails(
|
||||
val id: String,
|
||||
private val username: String,
|
||||
private val password: String,
|
||||
val group: GroupDto?,
|
||||
val permissions: Collection<Permission>,
|
||||
val isGroup: Boolean = false
|
||||
) : SpringUserDetails {
|
||||
constructor(user: UserDto) : this(user.id.toString(), user.fullName, user.password, user.group, user.permissions)
|
||||
|
||||
override fun getUsername() = username
|
||||
override fun getPassword() = password
|
||||
|
||||
@JsonIgnore
|
||||
override fun getAuthorities() = permissions.map { it.toAuthority() }.toMutableList()
|
||||
|
||||
override fun isAccountNonExpired() = true
|
||||
override fun isAccountNonLocked() = true
|
||||
override fun isCredentialsNonExpired() = true
|
||||
override fun isEnabled() = true
|
||||
}
|
||||
|
||||
data class UserLoginResponse(
|
||||
val id: String,
|
||||
val fullName: String,
|
||||
val groupId: Long?,
|
||||
val groupName: String?,
|
||||
val permissions: Collection<Permission>
|
||||
)
|
|
@ -5,7 +5,7 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
|||
import dev.fyloz.colorrecipesexplorer.dtos.*
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList
|
||||
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||
|
|
|
@ -2,13 +2,12 @@ package dev.fyloz.colorrecipesexplorer.logic
|
|||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
|
||||
import org.springframework.http.HttpStatus
|
||||
|
@ -46,4 +45,4 @@ class InvalidGroupStepsPositionsException(
|
|||
) {
|
||||
val errors: Set<InvalidPositionError>
|
||||
get() = exception.errors
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.Logic
|
||||
import dev.fyloz.colorrecipesexplorer.service.account.GroupService
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
interface GroupLogic : Logic<GroupDto, GroupService> {
|
||||
/** Gets all the users of the group with the given [id]. */
|
||||
fun getUsersForGroup(id: Long): Collection<UserDto>
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) :
|
||||
BaseLogic<GroupDto, GroupService>(service, Constants.ModelNames.GROUP),
|
||||
GroupLogic {
|
||||
override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id))
|
||||
|
||||
@Transactional
|
||||
override fun save(dto: GroupDto): GroupDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun update(dto: GroupDto): GroupDto {
|
||||
throwIfNameAlreadyExists(dto.name, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
|
||||
if (service.existsByName(name, id)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
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.logic.BaseLogic
|
||||
import dev.fyloz.colorrecipesexplorer.service.account.GroupTokenService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.parseBearer
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.util.*
|
||||
import javax.annotation.PostConstruct
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
interface GroupTokenLogic {
|
||||
fun isDisabled(id: String): Boolean
|
||||
fun getAll(): Collection<GroupTokenDto>
|
||||
fun getById(id: String): GroupTokenDto
|
||||
fun getById(id: UUID): GroupTokenDto
|
||||
fun getIdForRequest(request: HttpServletRequest): UUID?
|
||||
fun save(dto: GroupTokenSaveDto): GroupTokenDto
|
||||
fun enable(id: String): GroupTokenDto
|
||||
fun disable(id: String): GroupTokenDto
|
||||
fun deleteById(id: String)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultGroupTokenLogic(
|
||||
private val service: GroupTokenService,
|
||||
private val groupLogic: GroupLogic,
|
||||
private val jwtLogic: JwtLogic,
|
||||
private val enabledTokensCache: HashSet<String> = hashSetOf() // In constructor for unit testing
|
||||
) : GroupTokenLogic {
|
||||
private val typeName = Constants.ModelNames.GROUP_TOKEN
|
||||
private val typeNameLowerCase = typeName.lowercase()
|
||||
|
||||
@PostConstruct
|
||||
fun initEnabledTokensCache() {
|
||||
val tokensIds = getAll().filter { it.enabled }.map { it.id.toString() }
|
||||
enabledTokensCache.addAll(tokensIds)
|
||||
}
|
||||
|
||||
override fun isDisabled(id: String) = !enabledTokensCache.contains(id)
|
||||
override fun getAll() = service.getAll()
|
||||
override fun getById(id: String) = getById(UUID.fromString(id))
|
||||
override fun getById(id: UUID) = service.getById(id) ?: throw notFoundException(value = id)
|
||||
|
||||
override fun getIdForRequest(request: HttpServletRequest): UUID? {
|
||||
val groupTokenCookie = getGroupTokenCookie(request) ?: return null
|
||||
|
||||
val jwt = parseBearer(groupTokenCookie.value)
|
||||
return jwtLogic.parseGroupTokenIdJwt(jwt)
|
||||
}
|
||||
|
||||
override fun save(dto: GroupTokenSaveDto): GroupTokenDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
|
||||
val id = generateRandomUUID()
|
||||
val token = GroupTokenDto(
|
||||
id, dto.name, enabled = true, isDeleted = false, group = groupLogic.getById(dto.groupId)
|
||||
)
|
||||
|
||||
val savedToken = service.save(token)
|
||||
enabledTokensCache.add(savedToken.id.toString())
|
||||
|
||||
return savedToken
|
||||
}
|
||||
|
||||
override fun enable(id: String) = setEnabled(id, true).also {
|
||||
if (isDisabled(id)) {
|
||||
enabledTokensCache.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun disable(id: String) = setEnabled(id, false).also {
|
||||
if (!isDisabled(id)) {
|
||||
enabledTokensCache.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteById(id: String) {
|
||||
val token = getById(id).copy(enabled = false, isDeleted = true)
|
||||
|
||||
service.save(token)
|
||||
enabledTokensCache.remove(id)
|
||||
}
|
||||
|
||||
private fun setEnabled(id: String, enabled: Boolean) = with(getById(id)) {
|
||||
service.save(this.copy(enabled = enabled))
|
||||
}
|
||||
|
||||
private fun generateRandomUUID(): UUID {
|
||||
var uuid = UUID.randomUUID()
|
||||
|
||||
// The UUID specification doesn't guarantee to prevent collisions
|
||||
while (service.existsById(uuid)) {
|
||||
uuid = UUID.randomUUID()
|
||||
}
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String) {
|
||||
if (service.existsByName(name)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGroupTokenCookie(request: HttpServletRequest) =
|
||||
WebUtils.getCookie(request, Constants.CookieNames.GROUP_TOKEN)
|
||||
|
||||
private fun notFoundException(identifierName: String = BaseLogic.ID_IDENTIFIER_NAME, value: Any) =
|
||||
NotFoundException(
|
||||
typeNameLowerCase,
|
||||
"$typeName not found",
|
||||
"A $typeNameLowerCase with the $identifierName '$value' could not be found",
|
||||
value,
|
||||
identifierName
|
||||
)
|
||||
|
||||
private fun alreadyExistsException(identifierName: String = BaseLogic.NAME_IDENTIFIER_NAME, value: Any) =
|
||||
AlreadyExistsException(
|
||||
typeNameLowerCase,
|
||||
"$typeName already exists",
|
||||
"A $typeNameLowerCase with the $identifierName '$value' already exists",
|
||||
value,
|
||||
identifierName
|
||||
)
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
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.UserJwt
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.utils.base64encode
|
||||
import dev.fyloz.colorrecipesexplorer.utils.toDate
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.jackson.io.JacksonDeserializer
|
||||
import io.jsonwebtoken.jackson.io.JacksonSerializer
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
interface JwtLogic {
|
||||
/** Build a JWT for the given [userDetails]. */
|
||||
fun buildUserJwt(userDetails: UserDetails): String
|
||||
|
||||
/** Build a JWT for the given [groupTokenId]. */
|
||||
fun buildGroupTokenIdJwt(groupTokenId: UUID): String
|
||||
|
||||
/** Parses a user information from the given [jwt]. */
|
||||
fun parseUserJwt(jwt: String): UserJwt
|
||||
|
||||
/** Parses a group token id from the given [jwt]. */
|
||||
fun parseGroupTokenIdJwt(jwt: String): UUID
|
||||
}
|
||||
|
||||
@Service
|
||||
class DefaultJwtLogic(
|
||||
val objectMapper: ObjectMapper,
|
||||
val securityProperties: CreSecurityProperties
|
||||
) : JwtLogic {
|
||||
private val secretKey by lazy {
|
||||
securityProperties.jwtSecret.base64encode()
|
||||
}
|
||||
|
||||
private val permissionsById = Permission.values()
|
||||
.associateBy { it.id }
|
||||
|
||||
// Must be a new instance every time, or data from the last token will still be there
|
||||
private val jwtBuilder
|
||||
get() =
|
||||
Jwts.builder()
|
||||
.serializeToJsonWith(JacksonSerializer<Map<String, *>>(objectMapper))
|
||||
.signWith(secretKey)
|
||||
|
||||
private val jwtParser by lazy {
|
||||
Jwts.parserBuilder()
|
||||
.deserializeJsonWith(JacksonDeserializer<Map<String, *>>(objectMapper))
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun buildUserJwt(userDetails: UserDetails): String {
|
||||
val permissionsIds = userDetails.permissions.map { it.id }
|
||||
val type = if (userDetails.isGroup) JWT_TYPE_GROUP else JWT_TYPE_USER
|
||||
|
||||
return jwtBuilder
|
||||
.setSubject(userDetails.id)
|
||||
.setExpiration(getCurrentExpirationDate())
|
||||
.claim(JWT_CLAIM_PERMISSIONS, objectMapper.writeValueAsString(permissionsIds))
|
||||
.claim(JWT_CLAIM_TYPE, type)
|
||||
.compact()
|
||||
}
|
||||
|
||||
override fun buildGroupTokenIdJwt(groupTokenId: UUID): String =
|
||||
jwtBuilder
|
||||
.setSubject(groupTokenId.toString())
|
||||
.compact()
|
||||
|
||||
override fun parseUserJwt(jwt: String): UserJwt {
|
||||
val parsedJwt = jwtParser.parseClaimsJws(jwt)
|
||||
|
||||
val serializedPermissions = parsedJwt.body.get(JWT_CLAIM_PERMISSIONS, String::class.java)
|
||||
val permissionsIds = objectMapper.readValue<Collection<Int>>(serializedPermissions)
|
||||
val permissions = permissionsIds.map { permissionsById[it]!! }
|
||||
|
||||
val type = parsedJwt.body[JWT_CLAIM_TYPE] as Int
|
||||
val isGroup = type == JWT_TYPE_GROUP
|
||||
|
||||
val authorities = permissions
|
||||
.map { it.toAuthority() }
|
||||
.toMutableList()
|
||||
|
||||
return UserJwt(parsedJwt.body.subject, authorities, isGroup)
|
||||
}
|
||||
|
||||
override fun parseGroupTokenIdJwt(jwt: String): UUID {
|
||||
val uuid = jwtParser.parseClaimsJws(jwt).body.subject
|
||||
return UUID.fromString(uuid)
|
||||
}
|
||||
|
||||
private fun getCurrentExpirationDate(): Date =
|
||||
Instant.now()
|
||||
.plusSeconds(securityProperties.jwtDuration)
|
||||
.toDate()
|
||||
|
||||
companion object {
|
||||
private const val JWT_CLAIM_PERMISSIONS = "perms"
|
||||
private const val JWT_CLAIM_TYPE = "type"
|
||||
|
||||
private const val JWT_TYPE_USER = 0
|
||||
private const val JWT_TYPE_GROUP = 1
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
|
@ -15,7 +15,7 @@ import org.springframework.stereotype.Service
|
|||
|
||||
interface UserDetailsLogic : SpringUserDetailsService {
|
||||
/** Loads an [User] for the given [id]. */
|
||||
fun loadUserById(id: Long, isDefaultGroupUser: Boolean = true): UserDetails
|
||||
fun loadUserById(id: Long): UserDetails
|
||||
}
|
||||
|
||||
@Service
|
||||
|
@ -25,18 +25,14 @@ class DefaultUserDetailsLogic(
|
|||
) : UserDetailsLogic {
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
try {
|
||||
return loadUserById(username.toLong(), false)
|
||||
return loadUserById(username.toLong())
|
||||
} catch (ex: NotFoundException) {
|
||||
throw UsernameNotFoundException(username)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
|
||||
val user = userLogic.getById(
|
||||
id,
|
||||
isSystemUser = true,
|
||||
isDefaultGroupUser = isDefaultGroupUser
|
||||
)
|
||||
override fun loadUserById(id: Long): UserDetails {
|
||||
val user = userLogic.getById(id, isSystemUser = true)
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
||||
|
@ -69,10 +65,10 @@ class EmergencyUserDetailsLogic(
|
|||
}
|
||||
|
||||
override fun loadUserByUsername(username: String): SpringUserDetails {
|
||||
return loadUserById(username.toLong(), false)
|
||||
return loadUserById(username.toLong())
|
||||
}
|
||||
|
||||
override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
|
||||
override fun loadUserById(id: Long): UserDetails {
|
||||
val user = users.firstOrNull { it.id == id }
|
||||
?: throw UsernameNotFoundException(id.toString())
|
||||
|
|
@ -1,20 +1,18 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.authorizationCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.filters.blacklistedJwtTokens
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.Logic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import dev.fyloz.colorrecipesexplorer.service.account.UserService
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.time.LocalDateTime
|
||||
|
@ -25,13 +23,7 @@ interface UserLogic : Logic<UserDto, UserService> {
|
|||
fun getAllByGroup(group: GroupDto): Collection<UserDto>
|
||||
|
||||
/** Gets the user with the given [id]. */
|
||||
fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto
|
||||
|
||||
/** Gets the default user of the given [group]. */
|
||||
fun getDefaultGroupUser(group: GroupDto): UserDto
|
||||
|
||||
/** Save a default group user for the given [group]. */
|
||||
fun saveDefaultGroupUser(group: GroupDto)
|
||||
fun getById(id: Long, isSystemUser: Boolean): UserDto
|
||||
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: UserSaveDto): UserDto
|
||||
|
@ -59,30 +51,13 @@ interface UserLogic : Logic<UserDto, UserService> {
|
|||
class DefaultUserLogic(
|
||||
service: UserService, @Lazy private val groupLogic: GroupLogic, @Lazy private val passwordEncoder: PasswordEncoder
|
||||
) : BaseLogic<UserDto, UserService>(service, Constants.ModelNames.USER), UserLogic {
|
||||
override fun getAll() = service.getAll(isSystemUser = false, isDefaultGroupUser = false)
|
||||
override fun getAll() = service.getAll(false)
|
||||
|
||||
override fun getAllByGroup(group: GroupDto) = service.getAllByGroup(group)
|
||||
|
||||
override fun getById(id: Long) = getById(id, isSystemUser = false, isDefaultGroupUser = false)
|
||||
override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean) =
|
||||
service.getById(id, !isDefaultGroupUser, !isSystemUser) ?: throw notFoundException(value = id)
|
||||
|
||||
override fun getDefaultGroupUser(group: GroupDto) =
|
||||
service.getDefaultGroupUser(group) ?: throw notFoundException(identifierName = "groupId", value = group.id)
|
||||
|
||||
override fun saveDefaultGroupUser(group: GroupDto) {
|
||||
save(
|
||||
UserSaveDto(
|
||||
id = group.defaultGroupUserId,
|
||||
firstName = group.name,
|
||||
lastName = "User",
|
||||
password = group.name,
|
||||
groupId = group.id,
|
||||
permissions = listOf(),
|
||||
isDefaultGroupUser = true
|
||||
)
|
||||
)
|
||||
}
|
||||
override fun getById(id: Long) = getById(id, false)
|
||||
override fun getById(id: Long, isSystemUser: Boolean) =
|
||||
service.getById(id, isSystemUser) ?: throw notFoundException(value = id)
|
||||
|
||||
override fun save(dto: UserSaveDto) = save(
|
||||
UserDto(
|
||||
|
@ -92,8 +67,7 @@ class DefaultUserLogic(
|
|||
password = passwordEncoder.encode(dto.password),
|
||||
group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null,
|
||||
permissions = dto.permissions,
|
||||
isSystemUser = dto.isSystemUser,
|
||||
isDefaultGroupUser = dto.isDefaultGroupUser
|
||||
isSystemUser = dto.isSystemUser
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -105,7 +79,7 @@ class DefaultUserLogic(
|
|||
}
|
||||
|
||||
override fun update(dto: UserUpdateDto): UserDto {
|
||||
val user = getById(dto.id, isSystemUser = false, isDefaultGroupUser = false)
|
||||
val user = getById(dto.id)
|
||||
|
||||
return update(
|
||||
user.copy(
|
||||
|
@ -123,7 +97,7 @@ class DefaultUserLogic(
|
|||
return super.update(dto)
|
||||
}
|
||||
|
||||
override fun updateLastLoginTime(id: Long, time: LocalDateTime) = with(getById(id)) {
|
||||
override fun updateLastLoginTime(id: Long, time: LocalDateTime) = with(getById(id, true)) {
|
||||
update(this.copy(lastLoginTime = time))
|
||||
}
|
||||
|
||||
|
@ -140,7 +114,7 @@ class DefaultUserLogic(
|
|||
}
|
||||
|
||||
override fun logout(request: HttpServletRequest) {
|
||||
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
|
||||
val authorizationCookie = WebUtils.getCookie(request, Constants.CookieNames.AUTHORIZATION)
|
||||
if (authorizationCookie != null) {
|
||||
val authorizationToken = authorizationCookie.value
|
||||
if (authorizationToken != null && authorizationToken.startsWith("Bearer")) {
|
||||
|
@ -166,4 +140,4 @@ class DefaultUserLogic(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NoDefaultGroupException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.Logic
|
||||
import dev.fyloz.colorrecipesexplorer.service.GroupService
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.util.WebUtils
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
|
||||
|
||||
interface GroupLogic : Logic<GroupDto, GroupService> {
|
||||
/** Gets all the users of the group with the given [id]. */
|
||||
fun getUsersForGroup(id: Long): Collection<UserDto>
|
||||
|
||||
/** Gets the default group from a cookie in the given HTTP [request]. */
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto
|
||||
|
||||
/** Sets the default group cookie for the given HTTP [response]. */
|
||||
fun setResponseDefaultGroup(id: Long, response: HttpServletResponse)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) :
|
||||
BaseLogic<GroupDto, GroupService>(service, Constants.ModelNames.GROUP),
|
||||
GroupLogic {
|
||||
override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id))
|
||||
|
||||
override fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
?: throw NoDefaultGroupException()
|
||||
val defaultGroupUser = userLogic.getById(
|
||||
defaultGroupCookie.value.toLong(),
|
||||
isSystemUser = false,
|
||||
isDefaultGroupUser = true
|
||||
)
|
||||
return defaultGroupUser.group!!
|
||||
}
|
||||
|
||||
override fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) {
|
||||
val defaultGroupUser = userLogic.getDefaultGroupUser(getById(id))
|
||||
response.addHeader(
|
||||
"Set-Cookie",
|
||||
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict"
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun save(dto: GroupDto): GroupDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
|
||||
return super.save(dto).also {
|
||||
userLogic.saveDefaultGroupUser(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(dto: GroupDto): GroupDto {
|
||||
throwIfNameAlreadyExists(dto.name, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
userLogic.deleteById(GroupDto.getDefaultGroupUserId(id))
|
||||
super.deleteById(id)
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
|
||||
if (service.existsByName(name, id)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.utils.base64encode
|
||||
import dev.fyloz.colorrecipesexplorer.utils.toDate
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.jackson.io.JacksonDeserializer
|
||||
import io.jsonwebtoken.jackson.io.JacksonSerializer
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
const val jwtClaimUser = "user"
|
||||
|
||||
interface JwtLogic {
|
||||
/** Build a JWT token for the given [userDetails]. */
|
||||
fun buildJwt(userDetails: UserDetails): String
|
||||
|
||||
/** Build a JWT token for the given [user]. */
|
||||
fun buildJwt(user: UserDto): String
|
||||
|
||||
/** Parses a user from the given [jwt] token. */
|
||||
fun parseJwt(jwt: String): UserDto
|
||||
}
|
||||
|
||||
@Service
|
||||
class DefaultJwtLogic(
|
||||
val objectMapper: ObjectMapper,
|
||||
val securityProperties: CreSecurityProperties
|
||||
) : JwtLogic {
|
||||
private val secretKey by lazy {
|
||||
securityProperties.jwtSecret.base64encode()
|
||||
}
|
||||
|
||||
private val jwtBuilder by lazy {
|
||||
Jwts.builder()
|
||||
.serializeToJsonWith(JacksonSerializer<Map<String, *>>(objectMapper))
|
||||
.signWith(secretKey)
|
||||
}
|
||||
|
||||
private val jwtParser by lazy {
|
||||
Jwts.parserBuilder()
|
||||
.deserializeJsonWith(JacksonDeserializer<Map<String, *>>(objectMapper))
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun buildJwt(userDetails: UserDetails) =
|
||||
buildJwt(userDetails.user)
|
||||
|
||||
override fun buildJwt(user: UserDto): String =
|
||||
jwtBuilder
|
||||
.setSubject(user.id.toString())
|
||||
.setExpiration(getCurrentExpirationDate())
|
||||
.claim(jwtClaimUser, user.serialize())
|
||||
.compact()
|
||||
|
||||
override fun parseJwt(jwt: String): UserDto =
|
||||
with(
|
||||
jwtParser.parseClaimsJws(jwt)
|
||||
.body.get(jwtClaimUser, String::class.java)
|
||||
) {
|
||||
objectMapper.readValue(this)
|
||||
}
|
||||
|
||||
private fun getCurrentExpirationDate(): Date =
|
||||
Instant.now()
|
||||
.plusSeconds(securityProperties.jwtDuration)
|
||||
.toDate()
|
||||
|
||||
private fun UserDto.serialize(): String =
|
||||
objectMapper.writeValueAsString(this)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model.account
|
||||
|
||||
import java.util.UUID
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "group_token")
|
||||
data class GroupToken(
|
||||
@Id
|
||||
val id: UUID,
|
||||
|
||||
@Column(unique = true)
|
||||
val name: String,
|
||||
|
||||
@Column(name = "is_valid")
|
||||
val isValid: Boolean,
|
||||
|
||||
@Column(name = "deleted")
|
||||
val isDeleted: Boolean,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id")
|
||||
val group: Group
|
||||
)
|
|
@ -4,32 +4,34 @@ import org.springframework.security.core.GrantedAuthority
|
|||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
|
||||
enum class Permission(
|
||||
val impliedPermissions: List<Permission> = listOf(),
|
||||
val id: Int,
|
||||
private val impliedPermissions: List<Permission> = listOf(),
|
||||
val deprecated: Boolean = false
|
||||
) {
|
||||
READ_FILE,
|
||||
WRITE_FILE(listOf(READ_FILE)),
|
||||
READ_FILE(0),
|
||||
WRITE_FILE(1, listOf(READ_FILE)),
|
||||
|
||||
VIEW_RECIPES(listOf(READ_FILE)),
|
||||
VIEW_CATALOG(listOf(READ_FILE)),
|
||||
VIEW_USERS,
|
||||
VIEW_RECIPES(2, listOf(READ_FILE)),
|
||||
VIEW_CATALOG(3, listOf(READ_FILE)),
|
||||
VIEW_USERS(4),
|
||||
|
||||
EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)),
|
||||
EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)),
|
||||
EDIT_MATERIALS(listOf(VIEW_CATALOG, WRITE_FILE)),
|
||||
EDIT_MATERIAL_TYPES(listOf(VIEW_CATALOG)),
|
||||
EDIT_COMPANIES(listOf(VIEW_CATALOG)),
|
||||
EDIT_USERS(listOf(VIEW_USERS)),
|
||||
EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
|
||||
EDIT_RECIPES_PUBLIC_DATA(5, listOf(VIEW_RECIPES)),
|
||||
EDIT_RECIPES(6, listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)),
|
||||
EDIT_MATERIALS(7, listOf(VIEW_CATALOG, WRITE_FILE)),
|
||||
EDIT_MATERIAL_TYPES(8, listOf(VIEW_CATALOG)),
|
||||
EDIT_COMPANIES(9, listOf(VIEW_CATALOG)),
|
||||
EDIT_USERS(10, listOf(VIEW_USERS)),
|
||||
EDIT_CATALOG(11, listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
|
||||
|
||||
VIEW_TOUCH_UP_KITS,
|
||||
EDIT_TOUCH_UP_KITS(listOf(VIEW_TOUCH_UP_KITS)),
|
||||
VIEW_TOUCH_UP_KITS(12),
|
||||
EDIT_TOUCH_UP_KITS(13, listOf(VIEW_TOUCH_UP_KITS)),
|
||||
|
||||
PRINT_MIXES(listOf(VIEW_RECIPES)),
|
||||
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
|
||||
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
|
||||
PRINT_MIXES(14, listOf(VIEW_RECIPES)),
|
||||
ADD_TO_INVENTORY(15, listOf(VIEW_CATALOG)),
|
||||
DEDUCT_FROM_INVENTORY(16, listOf(VIEW_RECIPES)),
|
||||
|
||||
ADMIN(
|
||||
17,
|
||||
listOf(
|
||||
EDIT_RECIPES,
|
||||
EDIT_CATALOG,
|
||||
|
@ -44,58 +46,58 @@ enum class Permission(
|
|||
),
|
||||
|
||||
// deprecated permissions
|
||||
VIEW_RECIPE(listOf(VIEW_RECIPES), true),
|
||||
VIEW_MATERIAL(listOf(VIEW_CATALOG), true),
|
||||
VIEW_MATERIAL_TYPE(listOf(VIEW_CATALOG), true),
|
||||
VIEW_COMPANY(listOf(VIEW_CATALOG), true),
|
||||
VIEW(listOf(VIEW_RECIPES, VIEW_CATALOG), true),
|
||||
VIEW_EMPLOYEE(listOf(VIEW_USERS), true),
|
||||
VIEW_EMPLOYEE_GROUP(listOf(VIEW_USERS), true),
|
||||
VIEW_RECIPE(101, listOf(VIEW_RECIPES), true),
|
||||
VIEW_MATERIAL(102, listOf(VIEW_CATALOG), true),
|
||||
VIEW_MATERIAL_TYPE(103, listOf(VIEW_CATALOG), true),
|
||||
VIEW_COMPANY(104, listOf(VIEW_CATALOG), true),
|
||||
VIEW(105, listOf(VIEW_RECIPES, VIEW_CATALOG), true),
|
||||
VIEW_EMPLOYEE(106, listOf(VIEW_USERS), true),
|
||||
VIEW_EMPLOYEE_GROUP(107, listOf(VIEW_USERS), true),
|
||||
|
||||
EDIT_RECIPE(listOf(EDIT_RECIPES), true),
|
||||
EDIT_MATERIAL(listOf(EDIT_MATERIALS), true),
|
||||
EDIT_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPES), true),
|
||||
EDIT_COMPANY(listOf(EDIT_COMPANIES), true),
|
||||
EDIT(listOf(EDIT_RECIPES, EDIT_CATALOG), true),
|
||||
EDIT_EMPLOYEE(listOf(EDIT_USERS), true),
|
||||
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true),
|
||||
EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true),
|
||||
EDIT_RECIPE(108, listOf(EDIT_RECIPES), true),
|
||||
EDIT_MATERIAL(109, listOf(EDIT_MATERIALS), true),
|
||||
EDIT_MATERIAL_TYPE(110, listOf(EDIT_MATERIAL_TYPES), true),
|
||||
EDIT_COMPANY(111, listOf(EDIT_COMPANIES), true),
|
||||
EDIT(112, listOf(EDIT_RECIPES, EDIT_CATALOG), true),
|
||||
EDIT_EMPLOYEE(113, listOf(EDIT_USERS), true),
|
||||
EDIT_EMPLOYEE_PASSWORD(114, listOf(EDIT_USERS), true),
|
||||
EDIT_EMPLOYEE_GROUP(115, listOf(EDIT_USERS), true),
|
||||
|
||||
REMOVE_FILE(listOf(WRITE_FILE), true),
|
||||
GENERATE_TOUCH_UP_KIT(listOf(VIEW_TOUCH_UP_KITS), true),
|
||||
REMOVE_FILE(116, listOf(WRITE_FILE), true),
|
||||
GENERATE_TOUCH_UP_KIT(117, listOf(VIEW_TOUCH_UP_KITS), true),
|
||||
|
||||
REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE), true),
|
||||
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE), true),
|
||||
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES), true),
|
||||
REMOVE_COMPANIES(listOf(EDIT_COMPANIES), true),
|
||||
REMOVE_USERS(listOf(EDIT_USERS), true),
|
||||
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES), true),
|
||||
REMOVE_RECIPES(118, listOf(EDIT_RECIPES, REMOVE_FILE), true),
|
||||
REMOVE_MATERIALS(119, listOf(EDIT_MATERIALS, REMOVE_FILE), true),
|
||||
REMOVE_MATERIAL_TYPES(120, listOf(EDIT_MATERIAL_TYPES), true),
|
||||
REMOVE_COMPANIES(121, listOf(EDIT_COMPANIES), true),
|
||||
REMOVE_USERS(122, listOf(EDIT_USERS), true),
|
||||
REMOVE_CATALOG(123, listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES), true),
|
||||
|
||||
REMOVE_RECIPE(listOf(REMOVE_RECIPES), true),
|
||||
REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true),
|
||||
REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true),
|
||||
REMOVE_COMPANY(listOf(REMOVE_COMPANIES), true),
|
||||
REMOVE(listOf(REMOVE_RECIPES, REMOVE_CATALOG), true),
|
||||
REMOVE_EMPLOYEE(listOf(REMOVE_USERS), true),
|
||||
REMOVE_EMPLOYEE_GROUP(listOf(REMOVE_USERS), true),
|
||||
REMOVE_RECIPE(124, listOf(REMOVE_RECIPES), true),
|
||||
REMOVE_MATERIAL(125, listOf(REMOVE_MATERIALS), true),
|
||||
REMOVE_MATERIAL_TYPE(126, listOf(REMOVE_MATERIAL_TYPES), true),
|
||||
REMOVE_COMPANY(127, listOf(REMOVE_COMPANIES), true),
|
||||
REMOVE(128, listOf(REMOVE_RECIPES, REMOVE_CATALOG), true),
|
||||
REMOVE_EMPLOYEE(129, listOf(REMOVE_USERS), true),
|
||||
REMOVE_EMPLOYEE_GROUP(130, listOf(REMOVE_USERS), true),
|
||||
|
||||
SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true),
|
||||
SET_BROWSER_DEFAULT_GROUP(131, listOf(VIEW_USERS), true),
|
||||
;
|
||||
|
||||
operator fun contains(permission: Permission): Boolean {
|
||||
return permission == this || impliedPermissions.any { permission in it }
|
||||
}
|
||||
}
|
||||
|
||||
fun Permission.flat(): Iterable<Permission> {
|
||||
return mutableSetOf(this).apply {
|
||||
impliedPermissions.forEach {
|
||||
addAll(it.flat())
|
||||
fun flat(): Iterable<Permission> {
|
||||
return mutableSetOf(this).apply {
|
||||
impliedPermissions.forEach {
|
||||
addAll(it.flat())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Converts the given [Permission] to a [GrantedAuthority]. */
|
||||
fun Permission.toAuthority(): GrantedAuthority {
|
||||
return SimpleGrantedAuthority(name)
|
||||
/** Converts the given permission to a [GrantedAuthority]. */
|
||||
fun toAuthority(): GrantedAuthority {
|
||||
return SimpleGrantedAuthority(name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,34 @@
|
|||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.GroupToken
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Default group users are deprecated and should not be used anymore.
|
||||
* To prevent data loss, they will not be removed from the database,
|
||||
* but they are excluded from results from the database.
|
||||
*/
|
||||
@Repository
|
||||
interface UserRepository : JpaRepository<User, Long> {
|
||||
fun findAllByIsDefaultGroupUserIsFalse(): MutableList<User>
|
||||
|
||||
fun findByIdAndIsDefaultGroupUserIsFalse(id: Long): User?
|
||||
|
||||
/** Checks if a user with the given [firstName], [lastName] and a different [id] exists. */
|
||||
fun existsByFirstNameAndLastNameAndIdNot(firstName: String, lastName: String, id: Long): Boolean
|
||||
fun existsByFirstNameAndLastNameAndIdNotAndIsDefaultGroupUserIsFalse(firstName: String, lastName: String, id: Long): Boolean
|
||||
|
||||
/** Finds all users for the given [group]. */
|
||||
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isSystemUser IS FALSE AND u.isDefaultGroupUser IS FALSE")
|
||||
fun findAllByGroup(group: Group): Collection<User>
|
||||
|
||||
/** Finds the user with the given [firstName] and [lastName]. */
|
||||
@Query("SELECT u From User u WHERE u.firstName = :firstName AND u.lastName = :lastName")
|
||||
fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
|
||||
|
||||
/** Finds the default user for the given [group]. */
|
||||
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isDefaultGroupUser IS TRUE")
|
||||
fun findDefaultGroupUser(group: Group): User?
|
||||
}
|
||||
|
||||
@Repository
|
||||
|
@ -28,3 +36,15 @@ interface GroupRepository : JpaRepository<Group, Long> {
|
|||
/** Checks if a group with the given [name] and a different [id] exists. */
|
||||
fun existsByNameAndIdNot(name: String, id: Long): Boolean
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface GroupTokenRepository : JpaRepository<GroupToken, UUID> {
|
||||
/** Checks if a token that is not deleted with the given [name] exists. */
|
||||
fun existsByNameAndIsDeletedIsFalse(name: String): Boolean
|
||||
|
||||
/** Finds all group tokens that are not deleted. */
|
||||
fun findAllByIsDeletedIsFalse(): Collection<GroupToken>
|
||||
|
||||
/** Finds the group token with the given [id] if it is not deleted. */
|
||||
fun findByIdAndIsDeletedIsFalse(id: UUID): GroupToken?
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
|||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationBase
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
|
||||
import dev.fyloz.colorrecipesexplorer.restartApplication
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.rest.created
|
||||
import dev.fyloz.colorrecipesexplorer.rest.noContent
|
||||
import dev.fyloz.colorrecipesexplorer.rest.ok
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.validation.Valid
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.ControllerPaths.GROUP)
|
||||
@Profile("!emergency")
|
||||
class GroupController(
|
||||
private val groupLogic: GroupLogic
|
||||
) {
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
|
||||
fun getAll() =
|
||||
ok(groupLogic.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(groupLogic.getById(id))
|
||||
|
||||
@GetMapping("{id}/users")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getUsersForGroup(@PathVariable id: Long) =
|
||||
ok(groupLogic.getUsersForGroup(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody group: GroupDto) =
|
||||
created<GroupDto>(Constants.ControllerPaths.GROUP) {
|
||||
groupLogic.save(group)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody group: GroupDto) =
|
||||
noContent {
|
||||
groupLogic.update(group)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeEditUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
groupLogic.deleteById(id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeAdmin
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupTokenDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupTokenSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupTokenLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.rest.created
|
||||
import dev.fyloz.colorrecipesexplorer.rest.noContent
|
||||
import dev.fyloz.colorrecipesexplorer.rest.ok
|
||||
import dev.fyloz.colorrecipesexplorer.utils.addCookie
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.validation.Valid
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.ControllerPaths.GROUP_TOKEN)
|
||||
@PreAuthorizeAdmin
|
||||
@Profile("!emergency")
|
||||
class GroupTokenController(
|
||||
private val groupTokenLogic: GroupTokenLogic, private val jwtLogic: JwtLogic
|
||||
) {
|
||||
@GetMapping
|
||||
fun getAll() = ok(groupTokenLogic.getAll())
|
||||
|
||||
@GetMapping("current")
|
||||
fun getCurrent(request: HttpServletRequest): ResponseEntity<GroupTokenDto?> {
|
||||
val id = groupTokenLogic.getIdForRequest(request) ?: return ok(null)
|
||||
return ok(groupTokenLogic.getById(id))
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: String) = ok(groupTokenLogic.getById(id))
|
||||
|
||||
@PostMapping
|
||||
fun save(@RequestBody @Valid dto: GroupTokenSaveDto, response: HttpServletResponse) =
|
||||
with(groupTokenLogic.save(dto)) {
|
||||
addGroupTokenCookie(response, this)
|
||||
created(Constants.ControllerPaths.GROUP_TOKEN, this, this.id)
|
||||
}
|
||||
|
||||
@PutMapping("{id}/enable")
|
||||
fun enable(@PathVariable id: String) = noContent {
|
||||
groupTokenLogic.enable(id)
|
||||
}
|
||||
|
||||
@PutMapping("{id}/disable")
|
||||
fun disable(@PathVariable id: String) = noContent {
|
||||
groupTokenLogic.disable(id)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
fun deleteById(@PathVariable id: String) = noContent {
|
||||
groupTokenLogic.deleteById(id)
|
||||
}
|
||||
|
||||
private fun addGroupTokenCookie(response: HttpServletResponse, groupToken: GroupTokenDto) {
|
||||
val jwt = jwtLogic.buildGroupTokenIdJwt(groupToken.id)
|
||||
val bearer = Constants.BEARER_PREFIX + jwt
|
||||
|
||||
response.addCookie(Constants.CookieNames.GROUP_TOKEN, bearer) {
|
||||
httpOnly = GROUP_TOKEN_COOKIE_HTTP_ONLY
|
||||
sameSite = GROUP_TOKEN_COOKIE_SAME_SITE
|
||||
secure = !Constants.DEBUG_MODE
|
||||
maxAge = null // This cookie should never expire
|
||||
path = GROUP_TOKEN_COOKIE_PATH
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val GROUP_TOKEN_COOKIE_HTTP_ONLY = true
|
||||
private const val GROUP_TOKEN_COOKIE_SAME_SITE = true
|
||||
private const val GROUP_TOKEN_COOKIE_PATH = Constants.ControllerPaths.ACCOUNT_BASE_PATH
|
||||
}
|
||||
}
|
|
@ -1,21 +1,21 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
package dev.fyloz.colorrecipesexplorer.rest.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.rest.created
|
||||
import dev.fyloz.colorrecipesexplorer.rest.noContent
|
||||
import dev.fyloz.colorrecipesexplorer.rest.ok
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.validation.Valid
|
||||
|
||||
@RestController
|
||||
|
@ -78,74 +78,10 @@ class UserController(private val userLogic: UserLogic) {
|
|||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.ControllerPaths.GROUP)
|
||||
@Profile("!emergency")
|
||||
class GroupsController(
|
||||
private val groupLogic: GroupLogic,
|
||||
private val userLogic: UserLogic
|
||||
) {
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
|
||||
fun getAll() =
|
||||
ok(groupLogic.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(groupLogic.getById(id))
|
||||
|
||||
@GetMapping("{id}/users")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getUsersForGroup(@PathVariable id: Long) =
|
||||
ok(groupLogic.getUsersForGroup(id))
|
||||
|
||||
@PostMapping("default/{groupId}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse) =
|
||||
noContent {
|
||||
groupLogic.setResponseDefaultGroup(groupId, response)
|
||||
}
|
||||
|
||||
@GetMapping("default")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest) =
|
||||
ok(with(groupLogic) {
|
||||
getRequestDefaultGroup(request)
|
||||
})
|
||||
|
||||
@GetMapping("currentuser")
|
||||
fun getCurrentGroupUser(request: HttpServletRequest) =
|
||||
ok(with(groupLogic.getRequestDefaultGroup(request)) {
|
||||
userLogic.getDefaultGroupUser(this)
|
||||
})
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody group: GroupDto) =
|
||||
created<GroupDto>(Constants.ControllerPaths.GROUP) {
|
||||
groupLogic.save(group)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody group: GroupDto) =
|
||||
noContent {
|
||||
groupLogic.update(group)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeEditUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
groupLogic.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api")
|
||||
@RequestMapping(Constants.ControllerPaths.LOGOUT)
|
||||
@Profile("!emergency")
|
||||
class LogoutController(private val userLogic: UserLogic) {
|
||||
@GetMapping("logout")
|
||||
@GetMapping
|
||||
@PreAuthorize("isFullyAuthenticated()")
|
||||
fun logout(request: HttpServletRequest) =
|
||||
ok {
|
|
@ -8,6 +8,7 @@ import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
|||
import dev.fyloz.colorrecipesexplorer.model.Recipe
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.account.GroupService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.collections.lazyMap
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDate
|
||||
|
@ -89,4 +90,4 @@ class DefaultRecipeService(
|
|||
with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) {
|
||||
recipe.approbationDate?.plus(this)?.isBefore(LocalDate.now())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
package dev.fyloz.colorrecipesexplorer.service.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.flat
|
||||
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.BaseService
|
||||
import dev.fyloz.colorrecipesexplorer.service.Service
|
||||
|
||||
interface GroupService : Service<GroupDto, Group, GroupRepository> {
|
||||
/** Checks if a group with the given [name] and a different [id] exists. */
|
||||
|
@ -28,4 +29,4 @@ class DefaultGroupService(repository: GroupRepository) : BaseService<GroupDto, G
|
|||
|
||||
override fun flattenPermissions(group: Group) =
|
||||
group.permissions.flatMap { it.flat() }.filter { !it.deprecated }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupTokenDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.GroupToken
|
||||
import dev.fyloz.colorrecipesexplorer.repository.GroupTokenRepository
|
||||
import java.util.UUID
|
||||
|
||||
interface GroupTokenService {
|
||||
fun existsById(id: UUID): Boolean
|
||||
fun existsByName(name: String): Boolean
|
||||
fun getAll(): Collection<GroupTokenDto>
|
||||
fun getById(id: UUID): GroupTokenDto?
|
||||
fun save(token: GroupTokenDto): GroupTokenDto
|
||||
fun deleteById(id: UUID)
|
||||
|
||||
fun toDto(entity: GroupToken): GroupTokenDto
|
||||
fun toEntity(dto: GroupTokenDto): GroupToken
|
||||
}
|
||||
|
||||
@ServiceComponent
|
||||
class DefaultGroupTokenService(private val repository: GroupTokenRepository, private val groupService: GroupService) :
|
||||
GroupTokenService {
|
||||
override fun existsById(id: UUID) = repository.existsById(id)
|
||||
override fun existsByName(name: String) = repository.existsByNameAndIsDeletedIsFalse(name)
|
||||
|
||||
override fun getAll() = repository.findAllByIsDeletedIsFalse().map(::toDto)
|
||||
|
||||
override fun getById(id: UUID): GroupTokenDto? {
|
||||
val entity = repository.findByIdAndIsDeletedIsFalse(id)
|
||||
|
||||
return if (entity != null) toDto(entity) else null
|
||||
}
|
||||
|
||||
override fun save(token: GroupTokenDto): GroupTokenDto {
|
||||
val entity = repository.save(toEntity(token))
|
||||
return toDto(entity)
|
||||
}
|
||||
|
||||
override fun deleteById(id: UUID) = repository.deleteById(id)
|
||||
|
||||
override fun toDto(entity: GroupToken) =
|
||||
GroupTokenDto(entity.id, entity.name, entity.isValid, entity.isDeleted, groupService.toDto(entity.group))
|
||||
|
||||
override fun toEntity(dto: GroupTokenDto) =
|
||||
GroupToken(dto.id, dto.name, dto.enabled, dto.isDeleted, groupService.toEntity(dto.group))
|
||||
}
|
|
@ -1,55 +1,49 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
package dev.fyloz.colorrecipesexplorer.service.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.flat
|
||||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import dev.fyloz.colorrecipesexplorer.service.BaseService
|
||||
import dev.fyloz.colorrecipesexplorer.service.Service
|
||||
|
||||
interface UserService : Service<UserDto, User, UserRepository> {
|
||||
/** Checks if a user with the given [firstName] and [lastName] exists. */
|
||||
fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long? = null): Boolean
|
||||
|
||||
/** Gets all users, depending on [isSystemUser] and [isDefaultGroupUser]. */
|
||||
fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean): Collection<UserDto>
|
||||
/** Gets all users, depending on [isSystemUser]. */
|
||||
fun getAll(isSystemUser: Boolean): Collection<UserDto>
|
||||
|
||||
/** Gets all users for the given [group]. */
|
||||
fun getAllByGroup(group: GroupDto): Collection<UserDto>
|
||||
|
||||
/** Finds the user with the given [id], depending on [isSystemUser] and [isDefaultGroupUser]. */
|
||||
fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto?
|
||||
/** Finds the user with the given [id], depending on [isSystemUser]. */
|
||||
fun getById(id: Long, isSystemUser: Boolean): UserDto?
|
||||
|
||||
/** Finds the user with the given [firstName] and [lastName]. */
|
||||
fun getByFirstNameAndLastName(firstName: String, lastName: String): UserDto?
|
||||
|
||||
/** Find the default user for the given [group]. */
|
||||
fun getDefaultGroupUser(group: GroupDto): UserDto?
|
||||
}
|
||||
|
||||
@ServiceComponent
|
||||
class DefaultUserService(repository: UserRepository, private val groupService: GroupService) :
|
||||
BaseService<UserDto, User, UserRepository>(repository), UserService {
|
||||
override fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long?) =
|
||||
repository.existsByFirstNameAndLastNameAndIdNot(firstName, lastName, id ?: 0L)
|
||||
repository.existsByFirstNameAndLastNameAndIdNotAndIsDefaultGroupUserIsFalse(firstName, lastName, id ?: 0L)
|
||||
|
||||
override fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean) =
|
||||
repository.findAll()
|
||||
override fun getAll(isSystemUser: Boolean) =
|
||||
repository.findAllByIsDefaultGroupUserIsFalse()
|
||||
.filter { isSystemUser || !it.isSystemUser }
|
||||
.filter { isDefaultGroupUser || !it.isDefaultGroupUser }
|
||||
.map(::toDto)
|
||||
|
||||
override fun getAllByGroup(group: GroupDto) =
|
||||
repository.findAllByGroup(groupService.toEntity(group))
|
||||
.map(::toDto)
|
||||
|
||||
override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto? {
|
||||
val user = repository.findByIdOrNull(id) ?: return null
|
||||
if ((!isSystemUser && user.isSystemUser) ||
|
||||
!isDefaultGroupUser && user.isDefaultGroupUser
|
||||
) {
|
||||
override fun getById(id: Long, isSystemUser: Boolean): UserDto? {
|
||||
val user = repository.findByIdAndIsDefaultGroupUserIsFalse(id) ?: return null
|
||||
if (!isSystemUser && user.isSystemUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -61,11 +55,6 @@ class DefaultUserService(repository: UserRepository, private val groupService: G
|
|||
return if (user != null) toDto(user) else null
|
||||
}
|
||||
|
||||
override fun getDefaultGroupUser(group: GroupDto): UserDto? {
|
||||
val user = repository.findDefaultGroupUser(groupService.toEntity(group))
|
||||
return if (user != null) toDto(user) else null
|
||||
}
|
||||
|
||||
override fun toDto(entity: User) = UserDto(
|
||||
entity.id,
|
||||
entity.firstName,
|
||||
|
@ -75,7 +64,6 @@ class DefaultUserService(repository: UserRepository, private val groupService: G
|
|||
getFlattenPermissions(entity),
|
||||
entity.permissions,
|
||||
entity.lastLoginTime,
|
||||
entity.isDefaultGroupUser,
|
||||
entity.isSystemUser
|
||||
)
|
||||
|
||||
|
@ -84,7 +72,7 @@ class DefaultUserService(repository: UserRepository, private val groupService: G
|
|||
dto.firstName,
|
||||
dto.lastName,
|
||||
dto.password,
|
||||
dto.isDefaultGroupUser,
|
||||
false,
|
||||
dto.isSystemUser,
|
||||
if (dto.group != null) groupService.toEntity(dto.group) else null,
|
||||
dto.explicitPermissions,
|
||||
|
@ -98,6 +86,6 @@ class DefaultUserService(repository: UserRepository, private val groupService: G
|
|||
return perms + groupService.flattenPermissions(user.group)
|
||||
}
|
||||
|
||||
return perms
|
||||
return perms.distinctBy { it.id }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +1,40 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
private const val defaultCookieMaxAge = 3600L
|
||||
private const val defaultCookieHttpOnly = true
|
||||
private const val defaultCookieSameSite = true
|
||||
private const val defaultCookieSecure = true
|
||||
|
||||
data class CookieBuilderOptions(
|
||||
/** HTTP Only cookies cannot be access by Javascript clients. */
|
||||
var httpOnly: Boolean = defaultCookieHttpOnly,
|
||||
var httpOnly: Boolean = DEFAULT_HTTP_ONLY,
|
||||
|
||||
/** SameSite cookies are only sent in requests to their origin location. */
|
||||
var sameSite: Boolean = defaultCookieSameSite,
|
||||
var sameSite: Boolean = DEFAULT_SAME_SITE,
|
||||
|
||||
/** Secure cookies are only sent in HTTPS requests. */
|
||||
var secure: Boolean = defaultCookieSecure,
|
||||
var secure: Boolean = DEFAULT_SECURE,
|
||||
|
||||
/** Cookie's maximum age in seconds. */
|
||||
var maxAge: Long = defaultCookieMaxAge
|
||||
)
|
||||
var maxAge: Long? = DEFAULT_MAX_AGE,
|
||||
|
||||
/** The path for which the cookie will be sent. */
|
||||
var path: String = DEFAULT_PATH
|
||||
) {
|
||||
companion object {
|
||||
private const val DEFAULT_MAX_AGE = 3600L
|
||||
private const val DEFAULT_HTTP_ONLY = true
|
||||
private const val DEFAULT_SAME_SITE = true
|
||||
private const val DEFAULT_SECURE = true
|
||||
private const val DEFAULT_PATH = Constants.ControllerPaths.BASE_PATH
|
||||
}
|
||||
}
|
||||
|
||||
private enum class CookieBuilderOption(val optionName: String) {
|
||||
HTTP_ONLY("HttpOnly"),
|
||||
SAME_SITE("SameSite"),
|
||||
SECURE("Secure"),
|
||||
MAX_AGE("Max-Age")
|
||||
MAX_AGE("Max-Age"),
|
||||
PATH("Path")
|
||||
}
|
||||
|
||||
fun HttpServletResponse.addCookie(name: String, value: String, optionsBuilder: CookieBuilderOptions.() -> Unit) {
|
||||
|
@ -42,7 +51,8 @@ private fun buildCookie(name: String, value: String, optionsBuilder: CookieBuild
|
|||
}
|
||||
}
|
||||
|
||||
fun addOption(option: CookieBuilderOption, value: Any) {
|
||||
fun addOption(option: CookieBuilderOption, value: Any?) {
|
||||
if (value == null) return
|
||||
cookie.append("${option.optionName}=$value;")
|
||||
}
|
||||
|
||||
|
@ -50,6 +60,10 @@ private fun buildCookie(name: String, value: String, optionsBuilder: CookieBuild
|
|||
addBoolOption(CookieBuilderOption.SAME_SITE, options.sameSite)
|
||||
addBoolOption(CookieBuilderOption.SECURE, options.secure)
|
||||
addOption(CookieBuilderOption.MAX_AGE, options.maxAge)
|
||||
addOption(CookieBuilderOption.PATH, options.path)
|
||||
|
||||
return cookie.toString()
|
||||
}
|
||||
|
||||
fun parseBearer(source: String) =
|
||||
source.replace(Constants.BEARER_PREFIX, "").trim()
|
|
@ -1 +1 @@
|
|||
spring.jpa.show-sql=true
|
||||
spring.jpa.show-sql=false
|
||||
|
|
|
@ -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.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultJwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.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.dtos.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.DefaultJwtLogic
|
||||
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.buildJwt(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.buildJwt(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.buildJwt(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.buildJwt(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.buildJwt(user)
|
||||
val parsedUser = jwtService.parseJwt(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.*
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
|
@ -213,4 +214,4 @@ class DefaultRecipeLogicTest {
|
|||
mixLogicMock.updateLocations(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
|
||||
|
@ -57,4 +57,4 @@ class DefaultRecipeStepLogicTest {
|
|||
// Assert
|
||||
assertThrows<InvalidGroupStepsPositionsException> { recipeStepLogic.validateGroupInformationSteps(groupInfo) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultGroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.service.GroupService
|
||||
import dev.fyloz.colorrecipesexplorer.service.account.GroupService
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -25,9 +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 { saveDefaultGroupUser(any()) } just runs
|
||||
every { getById(any(), any()) } returns user
|
||||
every { deleteById(any()) } just runs
|
||||
}
|
||||
|
||||
|
@ -72,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
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 org.springframework.web.util.WebUtils
|
||||
import java.util.*
|
||||
import javax.servlet.http.Cookie
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import kotlin.test.*
|
||||
|
||||
class DefaultGroupTokenLogicTest {
|
||||
private val groupTokenServiceMock = mockk<GroupTokenService>()
|
||||
private val groupLogicMock = mockk<GroupLogic>()
|
||||
private val jwtLogicMock = mockk<JwtLogic>()
|
||||
|
||||
private val enabledTokenCache = hashSetOf<String>()
|
||||
|
||||
private val groupTokenLogic =
|
||||
spyk(DefaultGroupTokenLogic(groupTokenServiceMock, groupLogicMock, jwtLogicMock, 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, false, 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 getIdForRequest_normalBehavior_returnsGroupTokenIdFromRequest() {
|
||||
// Arrange
|
||||
val request = mockk<HttpServletRequest>()
|
||||
val cookie = mockk<Cookie> {
|
||||
every { value } returns "Bearer$groupTokenIdStr"
|
||||
}
|
||||
|
||||
mockkStatic(WebUtils::class) {
|
||||
every { WebUtils.getCookie(any(), Constants.CookieNames.GROUP_TOKEN) } returns cookie
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_normalBehavior_callsSaveInService() {
|
||||
// Arrange
|
||||
every { groupTokenServiceMock.existsByName(any()) } returns false
|
||||
every { groupTokenServiceMock.existsById(any()) } returns false
|
||||
every { groupTokenServiceMock.save(any()) } returns groupToken
|
||||
every { groupLogicMock.getById(any()) } returns group
|
||||
|
||||
// Act
|
||||
withMockRandomUUID {
|
||||
groupTokenLogic.save(groupTokenSaveDto)
|
||||
}
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
groupTokenServiceMock.save(groupToken)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_idAlreadyExists_generatesNewId() {
|
||||
// Arrange
|
||||
every { groupTokenServiceMock.existsByName(any()) } returns false
|
||||
every { groupTokenServiceMock.existsById(any()) } returnsMany listOf(true, false)
|
||||
every { groupTokenServiceMock.save(any()) } returns groupToken
|
||||
every { groupLogicMock.getById(any()) } returns group
|
||||
|
||||
val anotherGroupTokenId = UUID.nameUUIDFromBytes("Another unit test token".toByteArray())
|
||||
|
||||
// Act
|
||||
withMockRandomUUID(listOf(groupTokenId, anotherGroupTokenId)) {
|
||||
groupTokenLogic.save(groupTokenSaveDto)
|
||||
}
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
groupTokenServiceMock.save(match {
|
||||
it.id == anotherGroupTokenId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_normalBehavior_addsIdToEnabledTokensCache() {
|
||||
// Arrange
|
||||
every { groupTokenServiceMock.existsByName(any()) } returns false
|
||||
every { groupTokenServiceMock.existsById(any()) } returns false
|
||||
every { groupTokenServiceMock.save(any()) } returns groupToken
|
||||
every { groupLogicMock.getById(any()) } returns group
|
||||
|
||||
// Act
|
||||
withMockRandomUUID {
|
||||
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_savesDeletedTokenInService() {
|
||||
// Arrange
|
||||
every { groupTokenLogic.getById(any<String>()) } answers { groupToken }
|
||||
every { groupTokenServiceMock.save(any()) } answers { firstArg() }
|
||||
|
||||
// Act
|
||||
groupTokenLogic.deleteById(groupTokenIdStr)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
groupTokenServiceMock.save(match {
|
||||
it.id == groupTokenId && it.isDeleted
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteById_normalBehavior_removesIdFromEnabledTokensCache() {
|
||||
// Arrange
|
||||
every { groupTokenLogic.getById(any<String>()) } answers { groupToken }
|
||||
every { groupTokenServiceMock.save(any()) } answers { firstArg() }
|
||||
|
||||
// Act
|
||||
groupTokenLogic.deleteById(groupTokenIdStr)
|
||||
|
||||
// Assert
|
||||
assertFalse(enabledTokenCache.contains(groupTokenIdStr))
|
||||
}
|
||||
|
||||
private fun withMockRandomUUID(uuids: List<UUID>? = null, block: () -> Unit) {
|
||||
mockkStatic(UUID::class) {
|
||||
if (uuids == null) {
|
||||
every { UUID.randomUUID() } returns groupTokenId
|
||||
} else {
|
||||
every { UUID.randomUUID() } returnsMany uuids
|
||||
}
|
||||
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,13 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultUserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import dev.fyloz.colorrecipesexplorer.service.account.UserService
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -24,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
|
||||
|
@ -46,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())
|
||||
|
||||
|
@ -64,7 +60,7 @@ class DefaultUserLogicTest {
|
|||
|
||||
// Assert
|
||||
verify {
|
||||
userServiceMock.getAll(isSystemUser = false, isDefaultGroupUser = false)
|
||||
userServiceMock.getAll(isSystemUser = false)
|
||||
}
|
||||
confirmVerified(userServiceMock)
|
||||
}
|
||||
|
@ -90,7 +86,7 @@ class DefaultUserLogicTest {
|
|||
|
||||
// Assert
|
||||
verify {
|
||||
userLogic.getById(user.id, isSystemUser = false, isDefaultGroupUser = false)
|
||||
userLogic.getById(user.id, isSystemUser = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,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)
|
||||
}
|
||||
|
@ -110,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
|
||||
|
@ -210,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
|
||||
|
@ -303,4 +258,4 @@ class DefaultUserLogicTest {
|
|||
userLogic.update(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue