Skip to content

Commit

Permalink
Deduplicate logic with SaveUserDetails in api-gateway (#2577)
Browse files Browse the repository at this point in the history
- added an attibute for `SaveUserDetails`
- refactored method createNewIfRequired -- now it returns `User`
- `SaveUserDetails` is stored in `WebSession` for OAuth2
  • Loading branch information
nulls authored Sep 12, 2023
1 parent a4df183 commit fe776bc
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.saveourtool.save.info.OauthProviderInfo
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.WebSession
import reactor.core.publisher.Mono

/**
Expand Down Expand Up @@ -33,8 +34,14 @@ class SecurityInfoController(
* Endpoint that provides the information about the current logged-in user (powered by spring security and OAUTH)
*
* @param authentication
* @param session
* @return user information
*/
@GetMapping("/user")
fun currentUserName(authentication: Authentication?): Mono<String> = backendService.findNameByAuthentication(authentication)
fun currentUserName(
authentication: Authentication?,
session: WebSession,
): Mono<String> = authentication
?.let { principal -> backendService.findByPrincipal(principal, session).map { it.name } }
?: Mono.empty()
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,13 @@ private fun userStatusBasedAuthorizationDecision(
backendService: BackendService,
authentication: Mono<Authentication>,
authorizationContext: AuthorizationContext,
) = authentication.flatMap { backendService.findByAuthentication(it) }
) = authentication
.flatMap { principal ->
authorizationContext.exchange.session
.flatMap { session ->
backendService.findByPrincipal(principal, session)
}
}
.filter { it.isEnabled }
.flatMap { authorizationManagerAuthorizationDecision(authentication, authorizationContext) }
.defaultIfEmpty(AuthorizationDecision(false))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ package com.saveourtool.save.gateway.service
import com.saveourtool.save.authservice.utils.SaveUserDetails
import com.saveourtool.save.entities.User
import com.saveourtool.save.gateway.config.ConfigurationProperties
import com.saveourtool.save.utils.SAVE_USER_DETAILS_ATTIBUTE
import com.saveourtool.save.utils.orNotFound
import com.saveourtool.save.utils.switchIfEmptyToResponseException
import org.springframework.http.HttpStatus

import org.springframework.http.MediaType
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.toEntity
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.server.WebSession
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono
import java.security.Principal

/**
* A service to backend to lookup users in DB
Expand Down Expand Up @@ -47,44 +52,27 @@ class BackendService(
private fun findAuthenticationUserDetails(uri: String): Mono<SaveUserDetails> = webClient.get()
.uri(uri)
.retrieve()
.onStatus({ it.is4xxClientError }) {
Mono.error(ResponseStatusException(it.statusCode()))
}
.toEntity<SaveUserDetails>()
.flatMap { responseEntity ->
responseEntity.body.toMono().orNotFound { "Authentication body is empty" }
}
.getSaveUserDetails()

/**
* Find current user [SaveUserDetails] by [authentication].
* Find current user [SaveUserDetails] by [principal].
*
* @param authentication current user [Authentication]
* @param principal current user [Principal]
* @param session current [WebSession]
* @return current user [SaveUserDetails]
*/
fun findByAuthentication(authentication: Authentication): Mono<SaveUserDetails> = when (authentication) {
is UsernamePasswordAuthenticationToken -> findByName(authentication.name)
is OAuth2AuthenticationToken -> {
val source = authentication.authorizedClientRegistrationId
val nameInSource = authentication.name
findByOriginalLogin(source, nameInSource)
}
else -> Mono.empty()
}

/**
* Find current username by [authentication].
*
* @param authentication current user [Authentication]
* @return current username
*/
fun findNameByAuthentication(authentication: Authentication?): Mono<String> = when (authentication) {
is UsernamePasswordAuthenticationToken -> authentication.name.toMono()
is OAuth2AuthenticationToken -> {
val source = authentication.authorizedClientRegistrationId
val nameInSource = authentication.name
findByOriginalLogin(source, nameInSource).map { it.name }
}
else -> Mono.empty()
fun findByPrincipal(principal: Principal, session: WebSession): Mono<SaveUserDetails> = when (principal) {
is OAuth2AuthenticationToken -> session.getAttribute<SaveUserDetails>(SAVE_USER_DETAILS_ATTIBUTE)
.toMono()
.switchIfEmptyToResponseException(HttpStatus.INTERNAL_SERVER_ERROR) {
"Not found attribute $SAVE_USER_DETAILS_ATTIBUTE for ${OAuth2AuthenticationToken::class}"
}
is UsernamePasswordAuthenticationToken -> (principal.principal as? SaveUserDetails)
.toMono()
.switchIfEmptyToResponseException(HttpStatus.INTERNAL_SERVER_ERROR) {
"Unexpected principal type ${principal.principal.javaClass} in ${UsernamePasswordAuthenticationToken::class}"
}
else -> Mono.error(BadCredentialsException("Unsupported authentication type: ${principal::class}"))
}

/**
Expand All @@ -94,13 +82,18 @@ class BackendService(
* @param nameInSource
* @return empty [Mono]
*/
fun createNewIfRequired(source: String, nameInSource: String): Mono<Void> = webClient.post()
fun createNewIfRequired(source: String, nameInSource: String): Mono<SaveUserDetails> = webClient.post()
.uri("/internal/users/new/$source/$nameInSource")
.contentType(MediaType.APPLICATION_JSON)
.retrieve()
.getSaveUserDetails()

private fun WebClient.ResponseSpec.getSaveUserDetails(): Mono<SaveUserDetails> = this
.onStatus({ it.is4xxClientError }) {
Mono.error(ResponseStatusException(it.statusCode()))
}
.toBodilessEntity()
.then()
.toEntity<SaveUserDetails>()
.flatMap { responseEntity ->
responseEntity.body.toMono().orNotFound { "Authentication body is empty" }
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
package com.saveourtool.save.gateway.utils

import com.saveourtool.save.authservice.utils.SaveUserDetails
import com.saveourtool.save.gateway.service.BackendService
import com.saveourtool.save.utils.switchIfEmptyToResponseException
import org.springframework.cloud.gateway.filter.GatewayFilter
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono
import java.security.Principal

/**
Expand All @@ -28,7 +20,11 @@ class AuthorizationHeadersGatewayFilterFactory(
) : AbstractGatewayFilterFactory<Any>() {
override fun apply(config: Any?): GatewayFilter = GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain ->
exchange.getPrincipal<Principal>()
.flatMap { resolveSaveUser(it) }
.flatMap { principal ->
exchange.session.flatMap { session ->
backendService.findByPrincipal(principal, session)
}
}
.map { user ->
exchange.mutate()
.request { builder ->
Expand All @@ -42,14 +38,4 @@ class AuthorizationHeadersGatewayFilterFactory(
.defaultIfEmpty(exchange)
.flatMap { chain.filter(it) }
}

private fun resolveSaveUser(principal: Principal): Mono<SaveUserDetails> = when (principal) {
is OAuth2AuthenticationToken -> backendService.findByOriginalLogin(principal.authorizedClientRegistrationId, principal.name)
is UsernamePasswordAuthenticationToken -> (principal.principal as? SaveUserDetails)
.toMono()
.switchIfEmptyToResponseException(HttpStatus.INTERNAL_SERVER_ERROR) {
"Unexpected principal type ${principal.principal.javaClass} in ${UsernamePasswordAuthenticationToken::class}"
}
else -> Mono.error(BadCredentialsException("Unsupported authentication type: ${principal::class}"))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.saveourtool.save.gateway.utils

import com.saveourtool.save.gateway.service.BackendService
import com.saveourtool.save.utils.SAVE_USER_DETAILS_ATTIBUTE

import org.slf4j.LoggerFactory
import org.springframework.security.authentication.BadCredentialsException
Expand Down Expand Up @@ -29,6 +30,10 @@ class StoringServerAuthenticationSuccessHandler(
} else {
throw BadCredentialsException("Not supported authentication type ${authentication::class}")
}
return backendService.createNewIfRequired(source, nameInSource)
return backendService.createNewIfRequired(source, nameInSource).flatMap { saveUser ->
webFilterExchange.exchange.session.map {
it.attributes[SAVE_USER_DETAILS_ATTIBUTE] = saveUser
}
}.then()
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.saveourtool.save.backend.controllers.internal

import com.saveourtool.save.authservice.utils.SaveUserDetails
import com.saveourtool.save.backend.repository.OriginalLoginRepository
import com.saveourtool.save.backend.service.UserDetailsService
import com.saveourtool.save.domain.Role
import com.saveourtool.save.utils.blockingToMono

import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
Expand All @@ -24,33 +21,22 @@ typealias SaveUserDetailsResponse = ResponseEntity<SaveUserDetails>
@RequestMapping("/internal/users")
class UsersController(
private val userService: UserDetailsService,
private val originalLoginRepository: OriginalLoginRepository,
) {
private val logger = LoggerFactory.getLogger(javaClass)

/**
* Stores user in the DB with provided [name] with [roleForNewUser] as role.
* Stores user in the DB with provided [name] with default role.
* And add a link to [source] for created user
*
* @param source user source
* @param name user name
*/
@PostMapping("/new/{source}/{name}")
@Transactional
fun saveNewUserIfRequired(
@PathVariable source: String,
@PathVariable name: String,
) {
val userFind = originalLoginRepository.findByNameAndSource(name, source)

userFind?.user?.let {
logger.debug("User $name ($source) is already present in the DB")
} ?: run {
logger.info("Saving user $name ($source) with authorities $roleForNewUser to the DB")
val savedUser = userService.saveNewUser(name, roleForNewUser)
userService.addSource(savedUser, name, source)
): Mono<SaveUserDetailsResponse> = blockingToMono { userService.saveNewUserIfRequired(source, name) }
.map {
ResponseEntity.ok().body(SaveUserDetails(it))
}
}

/**
* Find user by name
Expand All @@ -61,9 +47,10 @@ class UsersController(
@GetMapping("/find-by-name/{userName}")
fun findByName(
@PathVariable userName: String,
): Mono<SaveUserDetailsResponse> = userService.findByName(userName).map {
ResponseEntity.ok().body(SaveUserDetails(it))
}
): Mono<SaveUserDetailsResponse> = blockingToMono { userService.findByName(userName) }
.map {
ResponseEntity.ok().body(SaveUserDetails(it))
}

/**
* Find user by name and source
Expand All @@ -76,11 +63,8 @@ class UsersController(
fun findByOriginalLogin(
@PathVariable source: String,
@PathVariable nameInSource: String,
): Mono<SaveUserDetailsResponse> = userService.findByOriginalLogin(nameInSource, source).map {
ResponseEntity.ok().body(SaveUserDetails(it))
}

companion object {
private val roleForNewUser = Role.VIEWER.asSpringSecurityRole()
}
): Mono<SaveUserDetailsResponse> = blockingToMono { userService.findByOriginalLogin(nameInSource, source) }
.map {
ResponseEntity.ok().body(SaveUserDetails(it))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ import com.saveourtool.save.domain.UserSaveStatus
import com.saveourtool.save.entities.OriginalLogin
import com.saveourtool.save.entities.User
import com.saveourtool.save.info.UserStatus
import com.saveourtool.save.utils.AVATARS_PACKS_DIR
import com.saveourtool.save.utils.AvatarType
import com.saveourtool.save.utils.blockingToMono
import com.saveourtool.save.utils.orNotFound
import com.saveourtool.save.utils.*
import org.slf4j.Logger

import org.springframework.security.core.Authentication
import org.springframework.stereotype.Service
Expand All @@ -39,9 +37,7 @@ class UserDetailsService(
* @param username
* @return spring's UserDetails retrieved from save's user found by provided values
*/
fun findByName(username: String) = blockingToMono {
userRepository.findByName(username)
}
fun findByName(username: String) = userRepository.findByName(username)

/**
* @param name
Expand All @@ -54,9 +50,8 @@ class UserDetailsService(
* @param source source (where the user identity is coming from)
* @return spring's UserDetails retrieved from save's user found by provided values
*/
fun findByOriginalLogin(username: String, source: String) = blockingToMono {
originalLoginRepository.findByNameAndSource(username, source)?.user
}
fun findByOriginalLogin(username: String, source: String) =
originalLoginRepository.findByNameAndSource(username, source)?.user

/**
* We change the version just to work-around the caching on the frontend
Expand Down Expand Up @@ -147,13 +142,32 @@ class UserDetailsService(
}
}

/**
* @param source
* @param name
* @return existed [User] or a new one
*/
@Transactional
fun saveNewUserIfRequired(source: String, name: String): User =
originalLoginRepository.findByNameAndSource(name, source)
?.user
?.also {
log.debug("User $name ($source) is already present in the DB")
}
?: run {
log.info {
"Saving user $name ($source) with authorities $roleForNewUser to the DB"
}
saveNewUser(name).also { savedUser ->
addSource(savedUser, name, source)
}
}

/**
* @param userNameCandidate
* @param userRole
* @return created [User]
*/
@Transactional
fun saveNewUser(userNameCandidate: String, userRole: String): User {
private fun saveNewUser(userNameCandidate: String): User {
val existedUser = userRepository.findByName(userNameCandidate)
val name = existedUser?.let {
val prefix = "$userNameCandidate$UNIQUE_NAME_SEPARATOR"
Expand All @@ -171,7 +185,7 @@ class UserDetailsService(
User(
name = name,
password = null,
role = userRole,
role = roleForNewUser,
status = UserStatus.CREATED,
)
)
Expand Down Expand Up @@ -265,6 +279,8 @@ class UserDetailsService(
}

companion object {
private val log: Logger = getLogger<UserDetailsService>()
private const val UNIQUE_NAME_SEPARATOR = "_"
private val roleForNewUser = Role.VIEWER.asSpringSecurityRole()
}
}
Loading

0 comments on commit fe776bc

Please sign in to comment.