#30 Increase JWT security by removing useless information, return useful information in the login response body instead.
continuous-integration/drone/push Build is passing Details

Remove default group users from code base.
This commit is contained in:
FyloZ 2022-04-28 21:29:28 -04:00
parent 37b597936b
commit 64349b25e9
Signed by: william
GPG Key ID: 835378AE9AF4AE97
18 changed files with 139 additions and 242 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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) =

View File

@ -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"
}
}

View File

@ -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())
}

View File

@ -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")
}

View File

@ -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

View File

@ -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>
)

View File

@ -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)

View File

@ -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>)

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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

View File

@ -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) =

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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
}