Move user infos to JWT tokens #19
|
@ -33,6 +33,8 @@ dependencies {
|
|||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3")
|
||||
implementation("javax.xml.bind:jaxb-api:2.3.0")
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
||||
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||
implementation("io.jsonwebtoken:jjwt-jackson:0.11.2")
|
||||
implementation("org.apache.poi:poi-ooxml:4.1.0")
|
||||
implementation("org.apache.pdfbox:pdfbox:2.0.4")
|
||||
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2")
|
||||
|
@ -58,9 +60,6 @@ dependencies {
|
|||
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
||||
runtimeOnly("org.postgresql:postgresql:42.2.16")
|
||||
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
|
||||
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
|
|
|
@ -4,10 +4,8 @@ import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
|||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.user
|
||||
import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import dev.fyloz.colorrecipesexplorer.service.users.UserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.service.users.UserService
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
@ -23,8 +21,6 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
|||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
|
@ -43,7 +39,7 @@ import javax.servlet.http.HttpServletResponse
|
|||
@EnableConfigurationProperties(CreSecurityProperties::class)
|
||||
class SecurityConfig(
|
||||
private val securityProperties: CreSecurityProperties,
|
||||
@Lazy private val userDetailsService: CreUserDetailsService,
|
||||
@Lazy private val userDetailsService: UserDetailsService,
|
||||
@Lazy private val userService: UserService,
|
||||
private val environment: Environment,
|
||||
private val logger: Logger
|
||||
|
@ -120,6 +116,7 @@ class SecurityConfig(
|
|||
@EnableConfigurationProperties(CreSecurityProperties::class)
|
||||
class EmergencySecurityConfig(
|
||||
private val securityProperties: CreSecurityProperties,
|
||||
private val userDetailsService: UserDetailsService,
|
||||
private val environment: Environment
|
||||
) : WebSecurityConfigurerAdapter() {
|
||||
init {
|
||||
|
@ -134,13 +131,8 @@ class EmergencySecurityConfig(
|
|||
fun passwordEncoder() =
|
||||
getPasswordEncoder()
|
||||
|
||||
override fun configure(auth: AuthenticationManagerBuilder) {
|
||||
assertRootUserNotNull(securityProperties)
|
||||
// Create in-memory root user
|
||||
auth.inMemoryAuthentication()
|
||||
.withUser(securityProperties.root!!.id.toString())
|
||||
.password(passwordEncoder().encode(securityProperties.root!!.password))
|
||||
.authorities(SimpleGrantedAuthority(Permission.ADMIN.name))
|
||||
override fun configure(authBuilder: AuthenticationManagerBuilder) {
|
||||
authBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
|
@ -154,7 +146,9 @@ class EmergencySecurityConfig(
|
|||
JwtAuthenticationFilter(authenticationManager(), securityProperties) { }
|
||||
)
|
||||
.addFilter(
|
||||
JwtAuthorizationFilter(securityProperties, authenticationManager(), this::loadUserById)
|
||||
JwtAuthorizationFilter(securityProperties, authenticationManager()) {
|
||||
userDetailsService.loadUserById(it, false)
|
||||
}
|
||||
)
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
|
@ -166,18 +160,6 @@ class EmergencySecurityConfig(
|
|||
http.cors()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUserById(id: Long): UserDetails {
|
||||
assertRootUserNotNull(securityProperties)
|
||||
if (id == securityProperties.root!!.id) {
|
||||
return UserDetails(user(
|
||||
id = id,
|
||||
password = securityProperties.root!!.password,
|
||||
permissions = mutableSetOf(Permission.ADMIN)
|
||||
))
|
||||
}
|
||||
throw UsernameNotFoundException(id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
|
|
|
@ -123,11 +123,10 @@ data class UserDetails(val user: User) : SpringUserDetails {
|
|||
|
||||
// ==== DSL ====
|
||||
fun user(
|
||||
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
|
||||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
password: String = passwordEncoder.encode("password"),
|
||||
password: String = "password",
|
||||
isDefaultGroupUser: Boolean = false,
|
||||
isSystemUser: Boolean = false,
|
||||
group: Group? = null,
|
||||
|
@ -146,6 +145,30 @@ fun user(
|
|||
lastLoginTime
|
||||
).apply(op)
|
||||
|
||||
fun user(
|
||||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
plainPassword: String = "password",
|
||||
isDefaultGroupUser: Boolean = false,
|
||||
isSystemUser: Boolean = false,
|
||||
group: Group? = null,
|
||||
permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
lastLoginTime: LocalDateTime? = null,
|
||||
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
|
||||
op: User.() -> Unit = {}
|
||||
) = User(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(plainPassword),
|
||||
isDefaultGroupUser,
|
||||
isSystemUser,
|
||||
group,
|
||||
permissions,
|
||||
lastLoginTime
|
||||
).apply(op)
|
||||
|
||||
fun userSaveDto(
|
||||
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
|
||||
id: Long = 0L,
|
||||
|
|
|
@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.rest
|
|||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import dev.fyloz.colorrecipesexplorer.service.GroupService
|
||||
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
||||
import dev.fyloz.colorrecipesexplorer.service.users.UserService
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
|
|
|
@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.model.account.Group
|
|||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.users
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.AbstractExternalNamedModelService
|
||||
import dev.fyloz.colorrecipesexplorer.service.ExternalNamedModelService
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.util.WebUtils
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.transaction.Transactional
|
||||
|
||||
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
|
||||
|
||||
interface GroupService :
|
||||
ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
|
||||
/** Gets all the users of the group with the given [id]. */
|
||||
fun getUsersForGroup(id: Long): Collection<User>
|
||||
|
||||
/** Gets the default group from a cookie in the given HTTP [request]. */
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest): Group
|
||||
|
||||
/** Sets the default group cookie for the given HTTP [response]. */
|
||||
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
class GroupServiceImpl(
|
||||
private val userService: UserService,
|
||||
groupRepository: GroupRepository
|
||||
) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
|
||||
groupRepository
|
||||
),
|
||||
GroupService {
|
||||
override fun idNotFoundException(id: Long) = groupIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
|
||||
override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
|
||||
override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
|
||||
|
||||
override fun Group.toOutput() = GroupOutputDto(
|
||||
this.id!!,
|
||||
this.name,
|
||||
this.permissions,
|
||||
this.flatPermissions
|
||||
)
|
||||
|
||||
override fun existsByName(name: String): Boolean = repository.existsByName(name)
|
||||
override fun getUsersForGroup(id: Long): Collection<User> =
|
||||
userService.getByGroup(getById(id))
|
||||
|
||||
@Transactional
|
||||
override fun save(entity: Group): Group {
|
||||
return super<AbstractExternalNamedModelService>.save(entity).apply {
|
||||
userService.saveDefaultGroupUser(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(entity: GroupUpdateDto): Group {
|
||||
val persistedGroup by lazy { getById(entity.id) }
|
||||
return update(with(entity) {
|
||||
Group(
|
||||
entity.id,
|
||||
if (name.isNotBlank()) entity.name else persistedGroup.name,
|
||||
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun delete(entity: Group) {
|
||||
userService.delete(userService.getDefaultGroupUser(entity))
|
||||
super.delete(entity)
|
||||
}
|
||||
|
||||
override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
?: throw NoDefaultGroupException()
|
||||
val defaultGroupUser = userService.getById(
|
||||
defaultGroupCookie.value.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = true
|
||||
)
|
||||
return defaultGroupUser.group!!
|
||||
}
|
||||
|
||||
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
|
||||
val group = getById(groupId)
|
||||
val defaultGroupUser = userService.getDefaultGroupUser(group)
|
||||
response.addHeader(
|
||||
"Set-Cookie",
|
||||
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict"
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.users
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.user
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
interface UserDetailsService : SpringUserDetailsService {
|
||||
/** Loads an [User] for the given [id]. */
|
||||
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
class UserDetailsServiceImpl(
|
||||
private val userService: UserService
|
||||
) : UserDetailsService {
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
try {
|
||||
return loadUserById(username.toLong(), true)
|
||||
} catch (ex: NotFoundException) {
|
||||
throw UsernameNotFoundException(username)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
|
||||
val user = userService.getById(
|
||||
id,
|
||||
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
|
||||
ignoreSystemUsers = false
|
||||
)
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("emergency")
|
||||
class EmergencyUserDetailsServiceImpl(
|
||||
securityProperties: CreSecurityProperties
|
||||
) : UserDetailsService {
|
||||
private val users: Set<User>
|
||||
|
||||
init {
|
||||
if (securityProperties.root == null) {
|
||||
throw NullPointerException("The root user has not been configured")
|
||||
}
|
||||
|
||||
users = setOf(
|
||||
// Add root user
|
||||
with(securityProperties.root!!) {
|
||||
user(
|
||||
id = this.id,
|
||||
plainPassword = this.password,
|
||||
permissions = mutableSetOf(Permission.ADMIN)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun loadUserByUsername(username: String): SpringUserDetails {
|
||||
return loadUserById(username.toLong(), true)
|
||||
}
|
||||
|
||||
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
|
||||
val user = users.firstOrNull { it.id == id }
|
||||
?: throw UsernameNotFoundException(id.toString())
|
||||
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
|
@ -1,23 +1,18 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
package dev.fyloz.colorrecipesexplorer.service.users
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
||||
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
|
||||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.AbstractExternalModelService
|
||||
import dev.fyloz.colorrecipesexplorer.service.ExternalModelService
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.time.LocalDateTime
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.transaction.Transactional
|
||||
|
||||
interface UserService :
|
||||
ExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository> {
|
||||
|
@ -55,29 +50,11 @@ interface UserService :
|
|||
fun logout(request: HttpServletRequest)
|
||||
}
|
||||
|
||||
interface GroupService :
|
||||
ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
|
||||
/** Gets all the users of the group with the given [id]. */
|
||||
fun getUsersForGroup(id: Long): Collection<User>
|
||||
|
||||
/** Gets the default group from a cookie in the given HTTP [request]. */
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest): Group
|
||||
|
||||
/** Sets the default group cookie for the given HTTP [response]. */
|
||||
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
|
||||
}
|
||||
|
||||
interface CreUserDetailsService : SpringUserDetailsService {
|
||||
/** Loads an [User] for the given [id]. */
|
||||
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
class UserServiceImpl(
|
||||
userRepository: UserRepository,
|
||||
@Lazy val groupService: GroupService,
|
||||
@Lazy val passwordEncoder: PasswordEncoder,
|
||||
) : AbstractExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository>(
|
||||
userRepository
|
||||
),
|
||||
|
@ -120,11 +97,11 @@ class UserServiceImpl(
|
|||
|
||||
override fun save(entity: UserSaveDto): User =
|
||||
save(with(entity) {
|
||||
User(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(password),
|
||||
user(
|
||||
id = id,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
plainPassword = password,
|
||||
isDefaultGroupUser = false,
|
||||
isSystemUser = false,
|
||||
group = if (groupId != null) groupService.getById(groupId) else null,
|
||||
|
@ -146,7 +123,7 @@ class UserServiceImpl(
|
|||
id = 1000000L + group.id!!,
|
||||
firstName = group.name,
|
||||
lastName = "User",
|
||||
password = passwordEncoder.encode(group.name),
|
||||
plainPassword = group.name,
|
||||
group = group,
|
||||
isDefaultGroupUser = true
|
||||
)
|
||||
|
@ -195,11 +172,11 @@ class UserServiceImpl(
|
|||
override fun updatePassword(id: Long, password: String): User {
|
||||
val persistedUser = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
return super.update(with(persistedUser) {
|
||||
User(
|
||||
user(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(password),
|
||||
plainPassword = password,
|
||||
isDefaultGroupUser,
|
||||
isSystemUser,
|
||||
group,
|
||||
|
@ -225,100 +202,3 @@ class UserServiceImpl(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
class GroupServiceImpl(
|
||||
private val userService: UserService,
|
||||
groupRepository: GroupRepository
|
||||
) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
|
||||
groupRepository
|
||||
),
|
||||
GroupService {
|
||||
override fun idNotFoundException(id: Long) = groupIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
|
||||
override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
|
||||
override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
|
||||
|
||||
override fun Group.toOutput() = GroupOutputDto(
|
||||
this.id!!,
|
||||
this.name,
|
||||
this.permissions,
|
||||
this.flatPermissions
|
||||
)
|
||||
|
||||
override fun existsByName(name: String): Boolean = repository.existsByName(name)
|
||||
override fun getUsersForGroup(id: Long): Collection<User> =
|
||||
userService.getByGroup(getById(id))
|
||||
|
||||
@Transactional
|
||||
override fun save(entity: Group): Group {
|
||||
return super<AbstractExternalNamedModelService>.save(entity).apply {
|
||||
userService.saveDefaultGroupUser(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(entity: GroupUpdateDto): Group {
|
||||
val persistedGroup by lazy { getById(entity.id) }
|
||||
return update(with(entity) {
|
||||
Group(
|
||||
entity.id,
|
||||
if (name.isNotBlank()) entity.name else persistedGroup.name,
|
||||
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun delete(entity: Group) {
|
||||
userService.delete(userService.getDefaultGroupUser(entity))
|
||||
super.delete(entity)
|
||||
}
|
||||
|
||||
override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
?: throw NoDefaultGroupException()
|
||||
val defaultGroupUser = userService.getById(
|
||||
defaultGroupCookie.value.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = true
|
||||
)
|
||||
return defaultGroupUser.group!!
|
||||
}
|
||||
|
||||
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
|
||||
val group = getById(groupId)
|
||||
val defaultGroupUser = userService.getDefaultGroupUser(group)
|
||||
response.addHeader(
|
||||
"Set-Cookie",
|
||||
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
class CreUserDetailsServiceImpl(
|
||||
private val userService: UserService
|
||||
) : CreUserDetailsService {
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
try {
|
||||
return loadUserById(username.toLong(), true)
|
||||
} catch (ex: NotFoundException) {
|
||||
throw UsernameNotFoundException(username)
|
||||
} catch (ex: NotFoundException) {
|
||||
throw UsernameNotFoundException(username)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
|
||||
val user = userService.getById(
|
||||
id,
|
||||
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
|
||||
ignoreSystemUsers = false
|
||||
)
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import io.jsonwebtoken.Claims
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import io.jsonwebtoken.io.Encoders
|
||||
|
@ -13,13 +14,13 @@ data class Jwt(
|
|||
val secret: String,
|
||||
val duration: Long? = null,
|
||||
val signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.HS512,
|
||||
val claims: Map<String, Any?> = mapOf()
|
||||
val claims: Map<JwtClaim, Any?> = mapOf()
|
||||
) {
|
||||
val token: String by lazy {
|
||||
val builder = Jwts.builder()
|
||||
.signWith(keyFromSecret(secret))
|
||||
.setSubject(subject)
|
||||
.addClaims(claims.filterValues { it != null })
|
||||
.addClaims(mappedClaims)
|
||||
|
||||
duration?.let {
|
||||
val expirationMs = System.currentTimeMillis() + it
|
||||
|
@ -30,6 +31,18 @@ data class Jwt(
|
|||
|
||||
builder.compact()
|
||||
}
|
||||
|
||||
private val mappedClaims: Map<String, String> by lazy {
|
||||
claims
|
||||
.filterValues { it != null }
|
||||
.mapKeys { it.key.claim }
|
||||
.mapValues { it.value.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
enum class JwtClaim(val claim: String) {
|
||||
GROUP_ID("groupId"),
|
||||
GROUP_NAME("groupName")
|
||||
}
|
||||
|
||||
/** Build a [Jwt] for the given [User]. */
|
||||
|
@ -39,8 +52,8 @@ fun User.buildJwt(secret: String, duration: Long?) =
|
|||
secret,
|
||||
duration,
|
||||
claims = mapOf(
|
||||
"groupId" to this.group?.id,
|
||||
"groupName" to this.group?.name
|
||||
JwtClaim.GROUP_ID to this.group?.id,
|
||||
JwtClaim.GROUP_NAME to this.group?.name
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -5,8 +5,9 @@ import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
|
|||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
|
||||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.users.*
|
||||
import org.junit.jupiter.api.*
|
||||
import org.springframework.mock.web.MockHttpServletResponse
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
|
@ -25,17 +26,17 @@ class UserServiceTest :
|
|||
AbstractExternalModelServiceTest<User, UserSaveDto, UserUpdateDto, UserService, UserRepository>() {
|
||||
private val passwordEncoder = BCryptPasswordEncoder()
|
||||
|
||||
override val entity: User = user(passwordEncoder, id = 0L)
|
||||
override val anotherEntity: User = user(passwordEncoder, id = 1L)
|
||||
private val entityDefaultGroupUser = user(passwordEncoder, id = 2L, isDefaultGroupUser = true)
|
||||
private val entitySystemUser = user(passwordEncoder, id = 3L, isSystemUser = true)
|
||||
override val entity: User = user(id = 0L, passwordEncoder = passwordEncoder)
|
||||
override val anotherEntity: User = user(id = 1L, passwordEncoder = passwordEncoder)
|
||||
private val entityDefaultGroupUser = user(id = 2L, isDefaultGroupUser = true, passwordEncoder = passwordEncoder)
|
||||
private val entitySystemUser = user(id = 3L, isSystemUser = true, passwordEncoder = passwordEncoder)
|
||||
private val group = group(id = 0L)
|
||||
override val entitySaveDto: UserSaveDto = spy(userSaveDto(passwordEncoder, id = 0L))
|
||||
override val entityUpdateDto: UserUpdateDto = spy(userUpdateDto(id = 0L))
|
||||
|
||||
override val repository: UserRepository = mock()
|
||||
private val groupService: GroupService = mock()
|
||||
override val service: UserService = spy(UserServiceImpl(repository, groupService, passwordEncoder))
|
||||
override val service: UserService = spy(UserServiceImpl(repository, groupService))
|
||||
|
||||
private val entitySaveDtoUser = User(
|
||||
entitySaveDto.id,
|
||||
|
@ -210,7 +211,7 @@ class GroupServiceTest :
|
|||
override val entityWithEntityName: Group = group(id = 2L, name = entity.name)
|
||||
|
||||
private val groupUserId = 1000000L + entity.id!!
|
||||
private val groupUser = user(BCryptPasswordEncoder(), id = groupUserId, group = entity)
|
||||
private val groupUser = user(passwordEncoder = BCryptPasswordEncoder(), id = groupUserId, group = entity)
|
||||
|
||||
@BeforeEach
|
||||
override fun afterEach() {
|
||||
|
@ -303,7 +304,7 @@ class GroupServiceTest :
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class UserUserDetailsServiceTest {
|
||||
private val userService: UserService = mock()
|
||||
private val service = spy(CreUserDetailsServiceImpl(userService))
|
||||
private val service = spy(UserDetailsServiceImpl(userService))
|
||||
|
||||
private val user = user(id = 0L)
|
||||
|
||||
|
@ -317,8 +318,8 @@ class UserUserDetailsServiceTest {
|
|||
@Test
|
||||
fun `loadUserByUsername() calls loadUserByUserId() with the given username as an id`() {
|
||||
whenever(userService.getById(eq(user.id), any(), any())).doReturn(user)
|
||||
doReturn(SpringUser(user.id.toString(), user.password, listOf())).whenever(service)
|
||||
.loadUserById(user.id)
|
||||
doReturn(UserDetails(user(id = user.id, plainPassword = user.password)))
|
||||
.whenever(service).loadUserById(user.id)
|
||||
|
||||
service.loadUserByUsername(user.id.toString())
|
||||
|
||||
|
|
Loading…
Reference in New Issue