Skip to content

Commit

Permalink
Reworked authorization source for user (#2344)
Browse files Browse the repository at this point in the history
  • Loading branch information
nulls authored Jul 24, 2023
1 parent e23284b commit fe3e0c6
Show file tree
Hide file tree
Showing 56 changed files with 258 additions and 534 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.saveourtool.save.gateway.controller

import com.saveourtool.save.gateway.utils.userName
import com.saveourtool.save.gateway.service.BackendService
import com.saveourtool.save.info.OauthProviderInfo
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
import org.springframework.web.bind.annotation.*
import java.security.Principal
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono

/**
* Controller that returns various public information
Expand All @@ -13,6 +17,7 @@ import java.security.Principal
@RequestMapping("/sec")
class SecurityInfoController(
private val clientRegistrationRepository: InMemoryReactiveClientRegistrationRepository,
private val backendService: BackendService,
) {
/**
* @return a list of [OauthProviderInfo] for all configured providers
Expand All @@ -30,9 +35,17 @@ class SecurityInfoController(
/**
* Endpoint that provides the information about the current logged-in user (powered by spring security and OAUTH)
*
* @param principal
* @param authentication
* @return user information
*/
@GetMapping("/user")
fun currentUserName(principal: Principal?): String? = principal?.userName()
fun currentUserName(authentication: Authentication?): Mono<String> = when (authentication) {
is UsernamePasswordAuthenticationToken -> authentication.name.toMono()
is OAuth2AuthenticationToken -> {
val source = authentication.authorizedClientRegistrationId
val nameInSource = authentication.name
backendService.findByOriginalLogin(source, nameInSource).map { it.username }
}
else -> Mono.empty()
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.saveourtool.save.gateway.service

import com.saveourtool.save.authservice.utils.IdentitySourceAwareUserDetails
import com.saveourtool.save.entities.User
import com.saveourtool.save.gateway.config.ConfigurationProperties
import com.saveourtool.save.utils.IdentitySourceAwareUserDetailsMixin

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.http.MediaType
import org.springframework.http.codec.json.Jackson2JsonEncoder
import org.springframework.security.core.userdetails.User as SpringUser
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.jackson2.CoreJackson2Module
import org.springframework.security.jackson2.SecurityJackson2Modules
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.toEntity
Expand All @@ -22,19 +20,14 @@ import reactor.core.publisher.Mono
@Service
class BackendService(
configurationProperties: ConfigurationProperties,
objectMapper: ObjectMapper,
) {
private val objectMapper = ObjectMapper()
.findAndRegisterModules()
.registerModule(CoreJackson2Module())
.addMixIn(IdentitySourceAwareUserDetails::class.java, IdentitySourceAwareUserDetailsMixin::class.java)
private val webClient = WebClient.create(configurationProperties.backend.url)
.mutate()
.codecs {
it.defaultCodecs().jackson2JsonEncoder(
Jackson2JsonEncoder(objectMapper)
)
private val springUserDetailsReader = objectMapper
.also {
it.registerModules(SecurityJackson2Modules.getModules(javaClass.classLoader))
}
.build()
.readerFor(SpringUser::class.java)
private val webClient = WebClient.create(configurationProperties.backend.url)

/**
* @param username
Expand All @@ -48,7 +41,7 @@ class BackendService(
}
.toEntity<String>()
.map {
objectMapper.readValue(it.body, UserDetails::class.java)
springUserDetailsReader.readValue(it.body)
}

/**
Expand All @@ -64,19 +57,19 @@ class BackendService(
}
.toEntity<String>()
.map {
objectMapper.readValue(it.body, UserDetails::class.java)
springUserDetailsReader.readValue(it.body)
}

/**
* Saves a new [User] in DB
*
* @param user
* @param source
* @param nameInSource
* @return empty [Mono]
*/
fun createNewIfRequired(user: User): Mono<Void> = webClient.post()
.uri("/internal/users/new")
fun createNewIfRequired(source: String, nameInSource: String): Mono<Void> = webClient.post()
.uri("/internal/users/new/$source/$nameInSource")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(user)
.retrieve()
.onStatus({ it.is4xxClientError }) {
Mono.error(ResponseStatusException(it.statusCode()))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ class ConvertAuthorizationHeaderGatewayFilterFactory(
exchange.getPrincipal<Principal>()
.flatMap { principal ->
when (principal) {
is OAuth2AuthenticationToken -> backendService.findByOriginalLogin(principal.authorizedClientRegistrationId, principal.userName())
is OAuth2AuthenticationToken -> backendService.findByOriginalLogin(principal.authorizedClientRegistrationId, principal.name)
.map { it.username to principal.authorizedClientRegistrationId }
is UsernamePasswordAuthenticationToken -> {
// Note: current authentication type we support only for save-api, which already set
// user source into X-Authorization-Source header, however, in general case
// we need to provide it here too, somehow
Mono.just(principal.userName() to null)
Mono.just(principal.name to null)
}
else -> Mono.error(BadCredentialsException("Unsupported authentication type: ${principal::class}"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.saveourtool.save.gateway.utils

import com.saveourtool.save.domain.Role
import com.saveourtool.save.entities.User
import com.saveourtool.save.gateway.service.BackendService
import com.saveourtool.save.info.UserStatus

import org.slf4j.LoggerFactory
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.security.web.server.WebFilterExchange
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import reactor.core.publisher.Mono
Expand All @@ -23,29 +22,13 @@ class StoringServerAuthenticationSuccessHandler(
webFilterExchange: WebFilterExchange,
authentication: Authentication
): Mono<Void> {
logger.info("Authenticated user ${authentication.userName()} with authentication type ${authentication::class}, will send data to backend")
logger.info("Authenticated user ${authentication.name} with authentication type ${authentication::class}, will send data to backend")

val user = authentication.toUser().apply {
// https://github.com/saveourtool/save-cloud/issues/583
// fixme: this sets a default role for a new user with minimal scope, however this way we discard existing role
// from authentication provider. In the future we may want to use this information and have a mapping of existing
// roles to save-cloud roles.
role = Role.VIEWER.asSpringSecurityRole()
val (source, nameInSource) = if (authentication is OAuth2AuthenticationToken) {
authentication.authorizedClientRegistrationId to authentication.principal.name
} else {
throw BadCredentialsException("Not supported authentication type ${authentication::class}")
}

return backendService.createNewIfRequired(user)
return backendService.createNewIfRequired(source, nameInSource)
}
}

/**
* @return [User] with data from this [Authentication]
*/
private fun Authentication.toUser(): User = User(
userName(),
null,
authorities.joinToString(",") { it.authority },
toIdentitySource(),
null,
status = UserStatus.CREATED,
originalLogins = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package com.saveourtool.save.authservice.config

import com.saveourtool.save.authservice.security.ConvertingAuthenticationManager
import com.saveourtool.save.authservice.security.CustomAuthenticationBasicConverter
import com.saveourtool.save.authservice.utils.roleHierarchy
import com.saveourtool.save.v1

Expand All @@ -16,12 +15,10 @@ import org.springframework.http.HttpStatus
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.AuthenticationWebFilter
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint

import javax.annotation.PostConstruct
Expand All @@ -32,7 +29,6 @@ import javax.annotation.PostConstruct
@Suppress("MISSING_KDOC_TOP_LEVEL", "MISSING_KDOC_CLASS_ELEMENTS", "MISSING_KDOC_ON_FUNCTION")
class WebSecurityConfig(
private val authenticationManager: ConvertingAuthenticationManager,
private val customAuthenticationBasicConverter: CustomAuthenticationBasicConverter,
@Autowired private var defaultMethodSecurityExpressionHandler: DefaultMethodSecurityExpressionHandler
) {
@Bean
Expand All @@ -57,12 +53,9 @@ class WebSecurityConfig(
// FixMe: Properly support CSRF protection https://github.com/saveourtool/save-cloud/issues/34
csrf().disable()
}
.addFilterBefore(
AuthenticationWebFilter(authenticationManager).apply {
setServerAuthenticationConverter(customAuthenticationBasicConverter)
},
SecurityWebFiltersOrder.HTTP_BASIC,
)
.httpBasic {
it.authenticationManager(authenticationManager)
}
.exceptionHandling {
it.authenticationEntryPoint(
HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package com.saveourtool.save.authservice.repository

import com.saveourtool.save.entities.User
import com.saveourtool.save.info.UserStatus
import com.saveourtool.save.utils.orNotFound
import com.saveourtool.save.utils.getLogger
import com.saveourtool.save.utils.warn
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Component

Expand All @@ -17,24 +18,25 @@ class AuthenticationUserRepository(
* @param name name of user
* @return user or null if no results have been found
*/
fun findByName(name: String): User? {
val record = namedParameterJdbcTemplate.queryForList(
"SELECT * FROM save_cloud.user WHERE name = :name",
mapOf("name" to name)
).singleOrNull()
.orNotFound {
"There is no user with name $name"
}
return record.toUserEntity()
fun findByName(name: String): User? = namedParameterJdbcTemplate.queryForList(
"SELECT * FROM save_cloud.user WHERE name = :name",
mapOf("name" to name)
).singleOrNull()?.toUserEntity() ?: run {
logger.warn {
"There is no user with name $name"
}
null
}

/**
* @return Entity [User] created from provided [Map]
*/
private fun Map<String, Any>.toUserEntity(): User {
val record = this
return User(
name = record["name"] as String,
password = record["password"] as String?,
role = record["role"] as String?,
source = record["source"] as String,
email = record["email"] as String?,
avatar = record["avatar"] as String?,
company = record["company"] as String?,
Expand All @@ -48,4 +50,9 @@ class AuthenticationUserRepository(
this.id = record["id"] as Long
}
}

companion object {
@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION")
private val logger = getLogger<AuthenticationUserRepository>()
}
}
Loading

0 comments on commit fe3e0c6

Please sign in to comment.