feature/#30-group-authentication #31
|
@ -16,7 +16,8 @@ class GroupTokenAuthenticationProvider(private val groupTokenLogic: GroupTokenLo
|
|||
val groupTokenId = parseGroupTokenId(groupAuthenticationToken.id)
|
||||
val groupToken = retrieveGroupToken(groupTokenId)
|
||||
|
||||
val userDetails = UserDetails(groupToken.id, groupToken.name, "", groupToken.group.id, groupToken.group.permissions)
|
||||
val userDetails =
|
||||
UserDetails(groupToken.id.toString(), groupToken.name, "", groupToken.group, groupToken.group.permissions)
|
||||
return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.config.security
|
|||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.filters.GroupTokenAuthenticationFilter
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.filters.UsernamePasswordAuthenticationFilter
|
||||
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.account.GroupTokenLogic
|
||||
|
@ -19,7 +19,6 @@ import org.springframework.context.annotation.Bean
|
|||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
|
@ -102,15 +101,14 @@ abstract class BaseSecurityConfig(
|
|||
BasicAuthenticationFilter::class.java
|
||||
)
|
||||
.addFilter(
|
||||
JwtAuthorizationFilter(jwtLogic, authenticationManager(), userDetailsLogic)
|
||||
JwtAuthorizationFilter(jwtLogic, authenticationManager())
|
||||
)
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
// .antMatchers("/api/config/**").permitAll() // Allow access to logo and icon
|
||||
// .antMatchers("/api/account/login/group").permitAll() // Allow access to login
|
||||
// .antMatchers("**").fullyAuthenticated()
|
||||
.antMatchers("**").permitAll()
|
||||
.antMatchers("/api/config/**").permitAll() // Allow access to logo and icon
|
||||
.antMatchers("/api/account/login/**").permitAll() // Allow access to login
|
||||
.antMatchers("**").fullyAuthenticated()
|
||||
|
||||
if (Constants.DEBUG_MODE) {
|
||||
http
|
||||
|
@ -130,7 +128,6 @@ abstract class BaseSecurityConfig(
|
|||
}
|
||||
}
|
||||
|
||||
@Order(2)
|
||||
@Configuration
|
||||
@Profile("!emergency")
|
||||
@EnableWebSecurity
|
||||
|
|
|
@ -27,7 +27,7 @@ class GroupTokenAuthenticationFilter(
|
|||
}
|
||||
|
||||
override fun afterSuccessfulAuthentication(userDetails: UserDetails) {
|
||||
logger.info("Successful login for group id '${userDetails.groupId}' using token '${userDetails.id}' (${userDetails.username})")
|
||||
logger.info("Successful login for group id '${userDetails.group!!.id}' using token '${userDetails.id}' (${userDetails.username})")
|
||||
}
|
||||
|
||||
private fun getGroupTokenCookie(request: HttpServletRequest) =
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
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
|
||||
|
@ -21,6 +23,8 @@ abstract class JwtAuthenticationFilter(
|
|||
AbstractAuthenticationProcessingFilter(
|
||||
AntPathRequestMatcher(filterProcessesUrl, HttpMethod.POST.toString())
|
||||
) {
|
||||
private val jacksonObjectMapper = jacksonObjectMapper()
|
||||
|
||||
override fun successfulAuthentication(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
|
@ -30,19 +34,14 @@ abstract class JwtAuthenticationFilter(
|
|||
val userDetails = auth.principal as UserDetails
|
||||
val token = jwtLogic.buildJwt(userDetails)
|
||||
|
||||
addAuthorizationHeaders(response, token)
|
||||
addAuthorizationCookie(response, token)
|
||||
addResponseBody(userDetails, response)
|
||||
|
||||
afterSuccessfulAuthentication(userDetails)
|
||||
}
|
||||
|
||||
protected abstract fun afterSuccessfulAuthentication(userDetails: UserDetails)
|
||||
|
||||
private fun addAuthorizationHeaders(response: HttpServletResponse, token: String) {
|
||||
response.addHeader(Constants.HeaderNames.ACCESS_CONTROL_EXPOSE_HEADERS, Constants.HeaderNames.AUTHORIZATION)
|
||||
response.addHeader(Constants.HeaderNames.AUTHORIZATION, "$BEARER_TOKEN_PREFIX $token")
|
||||
}
|
||||
|
||||
private fun addAuthorizationCookie(response: HttpServletResponse, token: String) {
|
||||
response.addCookie(Constants.CookieNames.AUTHORIZATION, BEARER_TOKEN_PREFIX + token) {
|
||||
httpOnly = AUTHORIZATION_COOKIE_HTTP_ONLY
|
||||
|
@ -53,11 +52,27 @@ abstract class JwtAuthenticationFilter(
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private const val BEARER_TOKEN_PREFIX = "Bearer"
|
||||
const val BEARER_TOKEN_PREFIX = "Bearer"
|
||||
}
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.security.filters
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.UserDetailsLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.account.UserJwt
|
||||
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.www.BasicAuthenticationFilter
|
||||
import org.springframework.web.util.WebUtils
|
||||
|
@ -17,60 +16,42 @@ import javax.servlet.http.HttpServletResponse
|
|||
|
||||
class JwtAuthorizationFilter(
|
||||
private val jwtLogic: JwtLogic,
|
||||
authenticationManager: AuthenticationManager,
|
||||
private val userDetailsLogic: UserDetailsLogic
|
||||
authenticationManager: AuthenticationManager
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
fun tryLoginFromBearer(): Boolean {
|
||||
val authorizationCookie = WebUtils.getCookie(request, Constants.HeaderNames.AUTHORIZATION)
|
||||
// Check for an authorization token cookie or header
|
||||
val authorizationToken = if (authorizationCookie != null)
|
||||
authorizationCookie.value
|
||||
else
|
||||
request.getHeader(Constants.HeaderNames.AUTHORIZATION)
|
||||
val authorizationCookie = WebUtils.getCookie(request, Constants.HeaderNames.AUTHORIZATION)
|
||||
|
||||
// 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
|
||||
// If there is no authorization cookie, the user is not authenticated
|
||||
if (authorizationCookie == null) {
|
||||
chain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
fun tryLoginFromDefaultGroupCookie() {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
if (defaultGroupCookie != null) {
|
||||
val authenticationToken = getAuthenticationToken(defaultGroupCookie.value)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
}
|
||||
val authorizationToken = authorizationCookie.value
|
||||
if (!isJwtValid(authorizationToken)) {
|
||||
chain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
if (!tryLoginFromBearer())
|
||||
tryLoginFromDefaultGroupCookie()
|
||||
|
||||
SecurityContextHolder.getContext().authentication = getAuthentication(authorizationToken)
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
|
||||
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
|
||||
// The authorization token is valid if it starts with "Bearer"
|
||||
private fun isJwtValid(authorizationToken: String) =
|
||||
authorizationToken.startsWith(JwtAuthenticationFilter.BEARER_TOKEN_PREFIX)
|
||||
|
||||
private fun getAuthentication(authorizationToken: String): Authentication? {
|
||||
return try {
|
||||
val user = jwtLogic.parseJwt(token.replace("Bearer", ""))
|
||||
getAuthenticationToken(user)
|
||||
val jwt = authorizationToken.replace(JwtAuthenticationFilter.BEARER_TOKEN_PREFIX, "").trim()
|
||||
val user = jwtLogic.parseJwt(jwt)
|
||||
|
||||
getAuthentication(user)
|
||||
} catch (_: ExpiredJwtException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthenticationToken(user: UserDetails) =
|
||||
private fun getAuthentication(user: UserJwt) =
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.springframework.security.core.Authentication
|
|||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
const val defaultGroupCookieName = "Default-Group"
|
||||
val blacklistedJwtTokens = mutableListOf<String>()
|
||||
|
||||
class UsernamePasswordAuthenticationFilter(
|
||||
|
@ -30,7 +29,7 @@ class UsernamePasswordAuthenticationFilter(
|
|||
}
|
||||
|
||||
override fun afterSuccessfulAuthentication(userDetails: UserDetails) {
|
||||
updateUserLoginTime(userDetails.id as Long)
|
||||
updateUserLoginTime(userDetails.id.toLong())
|
||||
logger.info("User ${userDetails.id} (${userDetails.username}) has logged in successfully")
|
||||
}
|
||||
|
||||
|
|
|
@ -16,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
|
||||
|
|
|
@ -17,8 +17,7 @@ data class UserDto(
|
|||
|
||||
val lastName: String,
|
||||
|
||||
@field:JsonIgnore
|
||||
val password: String = "",
|
||||
@field:JsonIgnore val password: String = "",
|
||||
|
||||
val group: GroupDto?,
|
||||
|
||||
|
@ -28,11 +27,7 @@ data class UserDto(
|
|||
|
||||
val lastLoginTime: LocalDateTime? = null,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isDefaultGroupUser: Boolean = false,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isSystemUser: Boolean = false
|
||||
@field:JsonIgnore val isSystemUser: Boolean = false
|
||||
) : EntityDto {
|
||||
@get:JsonIgnore
|
||||
val fullName = "$firstName $lastName"
|
||||
|
@ -41,35 +36,29 @@ data class UserDto(
|
|||
data class UserSaveDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String,
|
||||
@field:NotBlank val firstName: String,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String,
|
||||
@field:NotBlank val lastName: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Size(min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL)
|
||||
val password: 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,
|
||||
@field:JsonIgnore val isSystemUser: Boolean = false,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isDefaultGroupUser: Boolean = false
|
||||
@field:JsonIgnore val isDefaultGroupUser: Boolean = false
|
||||
)
|
||||
|
||||
data class UserUpdateDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String,
|
||||
@field:NotBlank val firstName: String,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String,
|
||||
@field:NotBlank val lastName: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
|
@ -79,24 +68,30 @@ data class UserUpdateDto(
|
|||
data class UserLoginRequestDto(val id: Long, val password: String)
|
||||
|
||||
class UserDetails(
|
||||
val id: Any,
|
||||
val id: String,
|
||||
private val username: String,
|
||||
private val password: String,
|
||||
val groupId: Long?,
|
||||
val group: GroupDto?,
|
||||
val permissions: Collection<Permission>
|
||||
) : SpringUserDetails {
|
||||
constructor(user: UserDto) : this(user.id, user.fullName, user.password, user.group?.id, user.permissions)
|
||||
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 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>
|
||||
)
|
|
@ -4,26 +4,14 @@ 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.exception.NoDefaultGroupException
|
||||
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
|
||||
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
|
||||
|
@ -32,32 +20,11 @@ class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic)
|
|||
GroupLogic {
|
||||
override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id))
|
||||
|
||||
override fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, Constants.CookieNames.GROUP_TOKEN)
|
||||
?: 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",
|
||||
"${Constants.CookieNames.GROUP_TOKEN}=${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)
|
||||
}
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun update(dto: GroupDto): GroupDto {
|
||||
|
@ -66,11 +33,6 @@ class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic)
|
|||
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)
|
||||
|
|
|
@ -4,11 +4,14 @@ 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.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
|
||||
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.security.core.GrantedAuthority
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
@ -19,8 +22,8 @@ interface JwtLogic {
|
|||
/** Build a JWT token for the given [userDetails]. */
|
||||
fun buildJwt(userDetails: UserDetails): String
|
||||
|
||||
/** Parses a user from the given [jwt] token. */
|
||||
fun parseJwt(jwt: String): UserDetails
|
||||
/** Parses a user information from the given [jwt] token. */
|
||||
fun parseJwt(jwt: String): UserJwt
|
||||
}
|
||||
|
||||
@Service
|
||||
|
@ -47,24 +50,32 @@ class DefaultJwtLogic(
|
|||
|
||||
override fun buildJwt(userDetails: UserDetails): String =
|
||||
jwtBuilder
|
||||
.setSubject(userDetails.id.toString())
|
||||
.setSubject(userDetails.id)
|
||||
.setExpiration(getCurrentExpirationDate())
|
||||
.claim(jwtClaimUser, userDetails.serialize())
|
||||
.claim(JWT_CLAIM_PERMISSIONS, objectMapper.writeValueAsString(userDetails.permissions))
|
||||
.compact()
|
||||
|
||||
override fun parseJwt(jwt: String): UserDetails =
|
||||
with(
|
||||
jwtParser.parseClaimsJws(jwt)
|
||||
.body.get(jwtClaimUser, String::class.java)
|
||||
) {
|
||||
objectMapper.readValue(this)
|
||||
}
|
||||
override fun parseJwt(jwt: String): UserJwt {
|
||||
val parsedJwt = jwtParser.parseClaimsJws(jwt)
|
||||
|
||||
val serializedPermissions = parsedJwt.body.get(JWT_CLAIM_PERMISSIONS, String::class.java)
|
||||
val permissions = objectMapper.readValue<Collection<Permission>>(serializedPermissions)
|
||||
|
||||
val authorities = permissions
|
||||
.map { it.toAuthority() }
|
||||
.toMutableList()
|
||||
|
||||
return UserJwt(parsedJwt.body.subject, authorities)
|
||||
}
|
||||
|
||||
private fun getCurrentExpirationDate(): Date =
|
||||
Instant.now()
|
||||
.plusSeconds(securityProperties.jwtDuration)
|
||||
.toDate()
|
||||
|
||||
private fun UserDetails.serialize(): String =
|
||||
objectMapper.writeValueAsString(this)
|
||||
companion object {
|
||||
private const val JWT_CLAIM_PERMISSIONS = "permissions"
|
||||
}
|
||||
}
|
||||
|
||||
data class UserJwt(val id: String, val authorities: Collection<GrantedAuthority>)
|
|
@ -32,11 +32,7 @@ class DefaultUserDetailsLogic(
|
|||
}
|
||||
|
||||
override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
|
||||
val user = userLogic.getById(
|
||||
id,
|
||||
isSystemUser = true,
|
||||
isDefaultGroupUser = isDefaultGroupUser
|
||||
)
|
||||
val user = userLogic.getById(id, isSystemUser = true)
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,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
|
||||
|
@ -57,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(
|
||||
|
@ -90,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
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -103,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, isSystemUser = false)
|
||||
|
||||
return update(
|
||||
user.copy(
|
||||
|
|
|
@ -1,28 +1,34 @@
|
|||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.GroupToken
|
||||
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.UUID
|
||||
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
|
||||
|
|
|
@ -21,8 +21,7 @@ import javax.validation.Valid
|
|||
@RequestMapping(Constants.ControllerPaths.GROUP)
|
||||
@Profile("!emergency")
|
||||
class GroupController(
|
||||
private val groupLogic: GroupLogic,
|
||||
private val userLogic: UserLogic
|
||||
private val groupLogic: GroupLogic
|
||||
) {
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
|
||||
|
@ -39,26 +38,6 @@ class GroupController(
|
|||
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) =
|
||||
|
|
|
@ -25,6 +25,8 @@ class GroupTokenController(private val groupTokenLogic: GroupTokenLogic) {
|
|||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: String) = ok(groupTokenLogic.getById(id))
|
||||
|
||||
// TODO Remove when group tokens will be fully implemented
|
||||
@Deprecated("Only use for testing purposes")
|
||||
@GetMapping("{id}/cookie")
|
||||
fun addCookieForId(@PathVariable id: String, response: HttpServletResponse) {
|
||||
val groupToken = groupTokenLogic.getById(id)
|
||||
|
|
|
@ -9,49 +9,42 @@ import dev.fyloz.colorrecipesexplorer.model.account.flat
|
|||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.BaseService
|
||||
import dev.fyloz.colorrecipesexplorer.service.Service
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -63,11 +56,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,
|
||||
|
@ -77,7 +65,6 @@ class DefaultUserService(repository: UserRepository, private val groupService: G
|
|||
getFlattenPermissions(entity),
|
||||
entity.permissions,
|
||||
entity.lastLoginTime,
|
||||
entity.isDefaultGroupUser,
|
||||
entity.isSystemUser
|
||||
)
|
||||
|
||||
|
@ -86,7 +73,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,
|
||||
|
|
|
@ -30,6 +30,4 @@ spring.jackson.deserialization.fail-on-null-for-primitives=true
|
|||
spring.jackson.default-property-inclusion=non_null
|
||||
spring.profiles.active=@spring.profiles.active@
|
||||
|
||||
spring.sql.init.continue-on-error=true
|
||||
|
||||
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
|
||||
spring.sql.init.continue-on-error=true
|
|
@ -25,7 +25,6 @@ class DefaultGroupLogicTest {
|
|||
every { getAllByGroup(any()) } returns listOf()
|
||||
every { getById(any(), any(), any()) } returns user
|
||||
every { getDefaultGroupUser(any()) } returns user
|
||||
every { saveDefaultGroupUser(any()) } just runs
|
||||
every { deleteById(any()) } just runs
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue