From 32206a8587308b9c5e6f16acbe73ede1d332dbea Mon Sep 17 00:00:00 2001 From: Nariman Abdullin Date: Tue, 19 Mar 2024 17:48:49 +0300 Subject: [PATCH 1/5] Added Gateway ### What's done: - added a new module gateway - added a new module authentication-utils --- authentication-utils/build.gradle.kts | 9 ++ .../template/authentication/AppUserDetails.kt | 100 ++++++++++++++++++ gateway/build.gradle.kts | 14 +++ ...uthorizationHeadersGatewayFilterFactory.kt | 44 ++++++++ .../template/gateway/GatewayApplication.kt | 14 +++ .../gateway/service/AppUserDetailsService.kt | 52 +++++++++ ...oringServerAuthenticationSuccessHandler.kt | 39 +++++++ gradle/libs.versions.toml | 5 + ...pring-boot-kotlin-configuration.gradle.kts | 1 + settings.gradle.kts | 2 + 10 files changed, 280 insertions(+) create mode 100644 authentication-utils/build.gradle.kts create mode 100644 authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt create mode 100644 gateway/build.gradle.kts create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/GatewayApplication.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/utils/StoringServerAuthenticationSuccessHandler.kt diff --git a/authentication-utils/build.gradle.kts b/authentication-utils/build.gradle.kts new file mode 100644 index 0000000..0e1fcf7 --- /dev/null +++ b/authentication-utils/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("com.saveourtool.template.build.spring-boot-kotlin-configuration") +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("com.fasterxml.jackson.core:jackson-annotations") + implementation(libs.kotlin.logging) +} \ No newline at end of file diff --git a/authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt b/authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt new file mode 100644 index 0000000..7af021d --- /dev/null +++ b/authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt @@ -0,0 +1,100 @@ +package com.saveourtool.template.authentication + +import com.fasterxml.jackson.annotation.JsonIgnore +import io.github.oshai.kotlinlogging.KotlinLogging.logger +import org.springframework.http.HttpHeaders +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.AuthorityUtils +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken + +class AppUserDetails( + val id: Long, + val name: String, + val role: String, +): UserDetails { + + /** + * @return [PreAuthenticatedAuthenticationToken] + */ + fun toPreAuthenticatedAuthenticationToken() = + PreAuthenticatedAuthenticationToken(this, null, authorities) + + /** + * Populates `X-Authorization-*` headers + * + * @param httpHeaders + */ + fun populateHeaders(httpHeaders: HttpHeaders) { + httpHeaders.set(AUTHORIZATION_ID, id.toString()) + httpHeaders.set(AUTHORIZATION_NAME, name) + httpHeaders.set(AUTHORIZATION_ROLES, role) + } + + @JsonIgnore + override fun getAuthorities(): MutableCollection = AuthorityUtils.commaSeparatedStringToAuthorityList(role) + + @JsonIgnore + override fun getPassword(): String? = null + + @JsonIgnore + override fun getUsername(): String = name + + @JsonIgnore + override fun isAccountNonExpired(): Boolean = true + + @JsonIgnore + override fun isAccountNonLocked(): Boolean = true + + @JsonIgnore + override fun isCredentialsNonExpired(): Boolean = true + + @JsonIgnore + override fun isEnabled(): Boolean = true + + @Suppress("UastIncorrectHttpHeaderInspection") + companion object { + @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") + private val log = logger {} + + /** + * `X-Authorization-Roles` used to specify application's user id + */ + const val AUTHORIZATION_ID = "X-Authorization-Id" + + /** + * `X-Authorization-Roles` used to specify application's username + */ + const val AUTHORIZATION_NAME = "X-Authorization-Name" + + /** + * `X-Authorization-Roles` used to specify application's user roles + */ + const val AUTHORIZATION_ROLES = "X-Authorization-Roles" + + /** + * An attribute to store application's user + */ + const val APPLICATION_USER_ATTRIBUTE = "application-user" + + /** + * @return [AppUserDetails] created from values in headers + */ + fun HttpHeaders.toAppUserDetails(): AppUserDetails? { + return AppUserDetails( + id = getSingleHeader(AUTHORIZATION_ID)?.toLong() ?: return logWarnAndReturnEmpty(AUTHORIZATION_ID), + name = getSingleHeader(AUTHORIZATION_NAME) ?: return logWarnAndReturnEmpty(AUTHORIZATION_NAME), + role = getSingleHeader(AUTHORIZATION_ROLES) ?: return logWarnAndReturnEmpty(AUTHORIZATION_ROLES), + ) + } + + private fun HttpHeaders.getSingleHeader(headerName: String) = get(headerName)?.singleOrNull() + + private fun logWarnAndReturnEmpty(missedHeaderName: String): T? { + log.debug { + "Header $missedHeaderName is not provided: skipping pre-authenticated save-user authentication" + } + return null + } + } +} diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts new file mode 100644 index 0000000..25de1cf --- /dev/null +++ b/gateway/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("com.saveourtool.template.build.spring-boot-kotlin-configuration") +} + +dependencies { + implementation(projects.common) + implementation(projects.authenticationUtils) + implementation(libs.springdoc.openapi.starter.common) + implementation(libs.springdoc.openapi.starter.webmvc.ui) + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("org.springframework.cloud:spring-cloud-starter-gateway") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") + implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") +} \ No newline at end of file diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt new file mode 100644 index 0000000..334b6e1 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + */ + +package com.saveourtool.template.gateway + +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.stereotype.Component +import org.springframework.web.server.ServerWebExchange +import java.security.Principal + +/** + * Filter, that mutate existing exchange, + * inserts user's info into Authorization headers instead of existing value, not paying attention to the credentials, + * since at this moment they are already checked by gateway. + */ +@Component +class AuthorizationHeadersGatewayFilterFactory( +// private val backendService: BackendService, +) : AbstractGatewayFilterFactory() { + override fun apply(config: Any?): GatewayFilter = GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain -> + exchange.getPrincipal() + .flatMap { principal -> + exchange.session.flatMap { session -> + backendService.findByPrincipal(principal, session) + } + } + .map { user -> + exchange.mutate() + .request { builder -> + builder.headers { headers: HttpHeaders -> + headers.remove(HttpHeaders.AUTHORIZATION) + user.populateHeaders(headers) + } + } + .build() + } + .defaultIfEmpty(exchange) + .flatMap { chain.filter(it) } + } +} \ No newline at end of file diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/GatewayApplication.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/GatewayApplication.kt new file mode 100644 index 0000000..53f7183 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/GatewayApplication.kt @@ -0,0 +1,14 @@ +package com.saveourtool.template.gateway + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +/** + * @since 2024-03-19 + */ +@SpringBootApplication +class GatewayApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt new file mode 100644 index 0000000..c964ccb --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt @@ -0,0 +1,52 @@ +package com.saveourtool.template.gateway.service + +import com.saveourtool.template.authentication.AppUserDetails +import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.toMono +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +/** + * A temporary cache for [AppUserDetails] + */ +class AppUserDetailsService { + private val reentrantReadWriteLock = ReentrantReadWriteLock() + private val idGenerator = AtomicLong() + private val storage: HashMap = hashMapOf() + + /** + * Saves a new [AppUserDetails] in DB + * + * @param source + * @param nameInSource + * @return [Mono] with saved [AppUserDetails] + */ + fun createNewIfRequired(source: String, nameInSource: String): Mono = reentrantReadWriteLock.write { + AppUserDetails( + id = idGenerator.incrementAndGet(), + name = nameInSource, + role = "VIEWER", + ) + .also { storage[it.id] = it } + .toMono() + } + + /** + * @param id [AppUserDetails.id] + * @return cached [AppUserDetails] or null + */ + fun get(id: Long): AppUserDetails? = reentrantReadWriteLock.read { + storage[id] + } + + /** + * Caches provided [appUserDetails] + * + * @param appUserDetails [AppUserDetails] + */ + fun save(appUserDetails: AppUserDetails): Unit = reentrantReadWriteLock.write { + storage[appUserDetails.id] = appUserDetails + } +} \ No newline at end of file diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/utils/StoringServerAuthenticationSuccessHandler.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/utils/StoringServerAuthenticationSuccessHandler.kt new file mode 100644 index 0000000..2dbb7f2 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/utils/StoringServerAuthenticationSuccessHandler.kt @@ -0,0 +1,39 @@ +package com.saveourtool.template.gateway.utils + +import com.saveourtool.template.authentication.AppUserDetails.Companion.APPLICATION_USER_ATTRIBUTE +import com.saveourtool.template.gateway.service.AppUserDetailsService + +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 + +/** + * [ServerAuthenticationSuccessHandler] that saves user data in database on successful login + */ +class StoringServerAuthenticationSuccessHandler( + private val appUserDetailsService: AppUserDetailsService, +) : ServerAuthenticationSuccessHandler { + private val logger = LoggerFactory.getLogger(javaClass) + + override fun onAuthenticationSuccess( + webFilterExchange: WebFilterExchange, + authentication: Authentication + ): Mono { + logger.info("Authenticated user ${authentication.name} with authentication type ${authentication::class}, will send data to backend") + + val (source, nameInSource) = if (authentication is OAuth2AuthenticationToken) { + authentication.authorizedClientRegistrationId to authentication.principal.name + } else { + throw BadCredentialsException("Not supported authentication type ${authentication::class}") + } + return appUserDetailsService.createNewIfRequired(source, nameInSource).flatMap { appUser -> + webFilterExchange.exchange.session.map { + it.attributes[APPLICATION_USER_ATTRIBUTE] = appUser + } + }.then() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d7edb02..88675c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,9 @@ java = "21" kotlin = "1.9.22" spring-boot = "3.2.3" +spring-cloud = "2023.0.0" springdoc = "2.3.0" +kotlin-logging = "6.0.3" [plugins] @@ -14,6 +16,9 @@ kotlin-serialization-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-ser spring-boot-gradle-plugin = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" } spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } +spring-cloud-dependencies = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" } springdoc-openapi-starter-common = { module = "org.springdoc:springdoc-openapi-starter-common", version.ref = "springdoc" } springdoc-openapi-starter-webflux-ui = { module = "org.springdoc:springdoc-openapi-starter-webflux-ui", version.ref = "springdoc" } springdoc-openapi-starter-webmvc-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" } + +kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "kotlin-logging" } diff --git a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts index 8d124f9..35f29b0 100644 --- a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts @@ -39,6 +39,7 @@ tasks.withType { dependencies { implementation(project.dependencies.enforcedPlatform(libs.spring.boot.dependencies)) + implementation(project.dependencies.platform(libs.spring.cloud.dependencies)) } tasks.withType { diff --git a/settings.gradle.kts b/settings.gradle.kts index c2ebeac..da1f4be 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,8 +24,10 @@ plugins { includeBuild("gradle/plugins") include("common") +include("authentication-utils") include("backend-webmvc") include("backend-webflux") +include("gateway") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 567f44e842b8eff8ea1ea864a9910b78d61e9432 Mon Sep 17 00:00:00 2001 From: Nariman Abdullin Date: Wed, 20 Mar 2024 16:22:12 +0300 Subject: [PATCH 2/5] WIP --- backend-webflux/build.gradle.kts | 5 +-- backend-webmvc/build.gradle.kts | 5 +-- common/build.gradle.kts | 3 ++ .../saveourtool/template/util/ReactorUtils.kt | 19 +++++++++ gateway/build.gradle.kts | 10 ++++- gateway/src/db/db.changelog.xml | 11 +++++ gateway/src/db/original-login.xml | 27 ++++++++++++ gateway/src/db/user.xml | 23 ++++++++++ ...uthorizationHeadersGatewayFilterFactory.kt | 2 + .../gateway/entities/OriginalLogin.kt | 25 +++++++++++ .../template/gateway/entities/User.kt | 23 ++++++++++ .../repository/OriginalLoginRepository.kt | 23 ++++++++++ .../gateway/repository/UserRepository.kt | 17 ++++++++ .../gateway/service/AppUserDetailsService.kt | 41 ++++++++++++++++++ .../template/gateway/service/UserService.kt | 42 +++++++++++++++++++ gradle/libs.versions.toml | 12 +++--- .../com/saveourtool/template/build/Utils.kt | 19 +++++++++ ...tlin-mpp-with-jvm-configuration.gradle.kts | 5 +-- ...pring-boot-kotlin-configuration.gradle.kts | 8 +--- 19 files changed, 295 insertions(+), 25 deletions(-) create mode 100644 common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt create mode 100644 gateway/src/db/db.changelog.xml create mode 100644 gateway/src/db/original-login.xml create mode 100644 gateway/src/db/user.xml create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/OriginalLogin.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/OriginalLoginRepository.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/UserRepository.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt diff --git a/backend-webflux/build.gradle.kts b/backend-webflux/build.gradle.kts index fc0f306..8100d3b 100644 --- a/backend-webflux/build.gradle.kts +++ b/backend-webflux/build.gradle.kts @@ -5,12 +5,11 @@ plugins { } dependencies { - implementation(libs.springdoc.openapi.starter.common) - implementation(libs.springdoc.openapi.starter.webflux.ui) + implementation(projects.common) + implementation("org.springdoc:springdoc-openapi-starter-webflux-ui") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") - implementation(projects.common) testImplementation("org.springframework.boot:spring-boot-starter-test") } diff --git a/backend-webmvc/build.gradle.kts b/backend-webmvc/build.gradle.kts index c819691..80ec805 100644 --- a/backend-webmvc/build.gradle.kts +++ b/backend-webmvc/build.gradle.kts @@ -5,11 +5,10 @@ plugins { } dependencies { - implementation(libs.springdoc.openapi.starter.common) - implementation(libs.springdoc.openapi.starter.webmvc.ui) + implementation(projects.common) + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") - implementation(projects.common) testImplementation("org.springframework.boot:spring-boot-starter-test") } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index a1671ca..350aa4f 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -9,6 +9,9 @@ kotlin { commonMain { dependencies { implementation(libs.kotlinx.serialization.core) + implementation("org.springframework:spring-web") + implementation("io.projectreactor:reactor-core") + implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") } } } diff --git a/common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt b/common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt new file mode 100644 index 0000000..534e0d0 --- /dev/null +++ b/common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt @@ -0,0 +1,19 @@ +/** + * Utility methods for working with Reactor publishers + */ + +package com.saveourtool.template.util + +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException +import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.switchIfEmpty + +/** + * @param status + * @param messageCreator + * @return original [Mono] or [Mono.error] with [status] otherwise + */ +fun Mono.switchIfEmptyToResponseException(status: HttpStatus, messageCreator: (() -> String?) = { null }) = switchIfEmpty { + Mono.error(ResponseStatusException(status, messageCreator())) +} diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts index 25de1cf..2e9208b 100644 --- a/gateway/build.gradle.kts +++ b/gateway/build.gradle.kts @@ -1,14 +1,20 @@ plugins { id("com.saveourtool.template.build.spring-boot-kotlin-configuration") + id("com.saveourtool.template.build.mysql-local-run-configuration") } dependencies { implementation(projects.common) implementation(projects.authenticationUtils) - implementation(libs.springdoc.openapi.starter.common) - implementation(libs.springdoc.openapi.starter.webmvc.ui) + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") implementation("org.springframework.cloud:spring-cloud-starter-gateway") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") +} + +mysqlLocalRun { + databaseName = "gateway" + liquibaseChangelogPath = project.layout.projectDirectory.file("src/db/db.changelog.xml") } \ No newline at end of file diff --git a/gateway/src/db/db.changelog.xml b/gateway/src/db/db.changelog.xml new file mode 100644 index 0000000..e896b1f --- /dev/null +++ b/gateway/src/db/db.changelog.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/gateway/src/db/original-login.xml b/gateway/src/db/original-login.xml new file mode 100644 index 0000000..57e8ddd --- /dev/null +++ b/gateway/src/db/original-login.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/gateway/src/db/user.xml b/gateway/src/db/user.xml new file mode 100644 index 0000000..9a1cad0 --- /dev/null +++ b/gateway/src/db/user.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt index 334b6e1..db89202 100644 --- a/gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/AuthorizationHeadersGatewayFilterFactory.kt @@ -4,6 +4,7 @@ package com.saveourtool.template.gateway +import com.saveourtool.template.gateway.service.AppUserDetailsService import org.springframework.cloud.gateway.filter.GatewayFilter import org.springframework.cloud.gateway.filter.GatewayFilterChain import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory @@ -19,6 +20,7 @@ import java.security.Principal */ @Component class AuthorizationHeadersGatewayFilterFactory( + private val appUserDetailsService: AppUserDetailsService, // private val backendService: BackendService, ) : AbstractGatewayFilterFactory() { override fun apply(config: Any?): GatewayFilter = GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain -> diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/OriginalLogin.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/OriginalLogin.kt new file mode 100644 index 0000000..f4ba006 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/OriginalLogin.kt @@ -0,0 +1,25 @@ +package com.saveourtool.template.gateway.entities + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +/** + * @property name + * @property user + * @property source + */ +@Entity +class OriginalLogin( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var source: String, + @ManyToOne + @JoinColumn(name = "user_id") + var user: User, +) diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt new file mode 100644 index 0000000..538e689 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt @@ -0,0 +1,23 @@ +package com.saveourtool.template.gateway.entities + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +/** + * @property name + * @property password *in plain text* + * @property role role of this user + * @property email email of user + */ +@Entity +class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var password: String?, + var role: String?, + var email: String? = null, +) diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/OriginalLoginRepository.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/OriginalLoginRepository.kt new file mode 100644 index 0000000..5f2dcd0 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/OriginalLoginRepository.kt @@ -0,0 +1,23 @@ +package com.saveourtool.template.gateway.repository + +import com.saveourtool.template.gateway.entities.OriginalLogin +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +/** + * Repository to access data about original user logins and sources + */ +@Repository +interface OriginalLoginRepository : JpaRepository{ + /** + * @param name + * @param source + * @return user or null if no results have been found + */ + fun findByNameAndSource(name: String, source: String): OriginalLogin? + + /** + * @param id id of user + */ + fun deleteByUserId(id: Long) +} diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/UserRepository.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/UserRepository.kt new file mode 100644 index 0000000..0835901 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/repository/UserRepository.kt @@ -0,0 +1,17 @@ +package com.saveourtool.template.gateway.repository + +import com.saveourtool.template.gateway.entities.User +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +/** + * Repository to access data about users + */ +@Repository +interface UserRepository : JpaRepository { + /** + * @param username + * @return user or null if no results have been found + */ + fun findByName(username: String): User? +} diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt index c964ccb..baa0eb8 100644 --- a/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/AppUserDetailsService.kt @@ -1,8 +1,19 @@ package com.saveourtool.template.gateway.service import com.saveourtool.template.authentication.AppUserDetails +import com.saveourtool.template.authentication.AppUserDetails.Companion.APPLICATION_USER_ATTRIBUTE +import com.saveourtool.template.util.switchIfEmptyToResponseException +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.ResponseStatusException +import org.springframework.web.server.WebSession import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.switchIfEmpty import reactor.kotlin.core.publisher.toMono +import java.security.Principal import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read @@ -11,6 +22,7 @@ import kotlin.concurrent.write /** * A temporary cache for [AppUserDetails] */ +@Component class AppUserDetailsService { private val reentrantReadWriteLock = ReentrantReadWriteLock() private val idGenerator = AtomicLong() @@ -33,6 +45,25 @@ class AppUserDetailsService { .toMono() } + /** + * Find current user [SaveUserDetails] by [principal]. + * + * @param principal current user [Principal] + * @param session current [WebSession] + * @return current user [SaveUserDetails] + */ + fun findByPrincipal(principal: Principal, session: WebSession): Mono = when (principal) { + is OAuth2AuthenticationToken -> session.getAppUserDetails().switchIfEmpty { + 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}")) + } + /** * @param id [AppUserDetails.id] * @return cached [AppUserDetails] or null @@ -49,4 +80,14 @@ class AppUserDetailsService { fun save(appUserDetails: AppUserDetails): Unit = reentrantReadWriteLock.write { storage[appUserDetails.id] = appUserDetails } + + private fun WebSession.getAppUserDetails(): Mono = this + .getAttribute(APPLICATION_USER_ATTRIBUTE) + .toMono() + .switchIfEmptyToResponseException(HttpStatus.INTERNAL_SERVER_ERROR) { + "Not found attribute $APPLICATION_USER_ATTRIBUTE for ${OAuth2AuthenticationToken::class}" + } + .mapNotNull { id -> + get(id) + } } \ No newline at end of file diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt new file mode 100644 index 0000000..d68fa4d --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt @@ -0,0 +1,42 @@ +package com.saveourtool.template.gateway.service + +import com.saveourtool.template.gateway.entities.User +import com.saveourtool.template.gateway.repository.OriginalLoginRepository +import com.saveourtool.template.gateway.repository.UserRepository +import org.springframework.transaction.annotation.Transactional + +/** + * Service for [User] + */ +open class UserService( + private val userRepository: UserRepository, + private val originalLoginRepository: OriginalLoginRepository, +) { + /** + * @param username + * @param source source (where the user identity is coming from) + * @return existed [User] + */ + fun findByOriginalLogin(username: String, source: String): User? = + originalLoginRepository.findByNameAndSource(username, source)?.user + + /** + * @param source source (where the user identity is coming from) + * @param nameInSource name provided by source + * @return existed [User] or a new created [User] + */ + @Transactional + open fun getOrCreateNew( + source: String, + nameInSource: String, + ): User { + originalLoginRepository.findByNameAndSource(nameInSource, source) + ?.user + ?: run { + val newUser = User( + + ) + userRepository.save(newUser) + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64be085..53b5cf9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,8 @@ kotlin-wrappers = "1.0.0-pre.715" kotlin-serialization = "1.6.3" spring-boot = "3.2.3" spring-cloud = "2023.0.0" -springdoc = "2.3.0" +spring-data = "2023.1.4" +springdoc = "2.4.0" kotlin-logging = "6.0.3" [plugins] @@ -16,11 +17,10 @@ kotlin-multiplatform-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gra kotlin-serialization-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } spring-boot-gradle-plugin = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" } -spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } -spring-cloud-dependencies = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" } -springdoc-openapi-starter-common = { module = "org.springdoc:springdoc-openapi-starter-common", version.ref = "springdoc" } -springdoc-openapi-starter-webflux-ui = { module = "org.springdoc:springdoc-openapi-starter-webflux-ui", version.ref = "springdoc" } -springdoc-openapi-starter-webmvc-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" } +spring-boot-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } +spring-cloud-bom = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" } +spring-data-bom = { module = "org.springframework.data:spring-data-bom", version.ref = "spring-data" } +springdoc-openapi-bom = { module = "org.springdoc:springdoc-openapi", version.ref = "springdoc" } kotlin-wrappers-bom = { module = "org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom", version.ref = "kotlin-wrappers" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlin-serialization"} diff --git a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/Utils.kt b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/Utils.kt index 0f8f394..e1a7c1f 100644 --- a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/Utils.kt +++ b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/Utils.kt @@ -1,3 +1,22 @@ package com.saveourtool.template.build +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.the + fun kotlinw(target: String): String = "org.jetbrains.kotlin-wrappers:kotlin-$target" + +internal fun addAllSpringRelatedBoms( + project: Project, + implementation: (Provider) -> Dependency?, +) { + @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") + val libs = project.the() + implementation(project.dependencies.enforcedPlatform(libs.spring.boot.bom)) + implementation(project.dependencies.platform(libs.spring.cloud.bom)) + implementation(project.dependencies.platform(libs.spring.data.bom)) + implementation(project.dependencies.platform(libs.springdoc.openapi.bom)) +} \ No newline at end of file diff --git a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/kotlin-mpp-with-jvm-configuration.gradle.kts b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/kotlin-mpp-with-jvm-configuration.gradle.kts index fd37296..28a784d 100644 --- a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/kotlin-mpp-with-jvm-configuration.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/kotlin-mpp-with-jvm-configuration.gradle.kts @@ -8,16 +8,13 @@ plugins { kotlin("multiplatform") } -@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") -val libs = the() - kotlin { jvm() sourceSets { jvmMain { dependencies { - implementation(project.dependencies.enforcedPlatform(libs.spring.boot.dependencies)) + addAllSpringRelatedBoms(project, ::implementation) } } } diff --git a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts index bcdf27f..7833ccd 100644 --- a/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com/saveourtool/template/build/spring-boot-kotlin-configuration.gradle.kts @@ -1,10 +1,7 @@ package com.saveourtool.template.build -import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.* -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") @@ -16,11 +13,8 @@ plugins { configureKotlinCompile() -@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") -val libs = the() dependencies { - implementation(project.dependencies.enforcedPlatform(libs.spring.boot.dependencies)) - implementation(project.dependencies.platform(libs.spring.cloud.dependencies)) + addAllSpringRelatedBoms(project, ::implementation) } tasks.withType { From 11005d373bd966e8abafac86380d1d74f7e7e97b Mon Sep 17 00:00:00 2001 From: Nariman Abdullin Date: Wed, 20 Mar 2024 18:25:29 +0300 Subject: [PATCH 3/5] WIP 2 --- .../saveourtool/template/util/ReactorUtils.kt | 23 ++++++++ .../template/gateway/entities/Role.kt | 45 ++++++++++++++ .../template/gateway/entities/User.kt | 8 ++- .../gateway/security/WebSecurityConfig.kt | 58 +++++++++++++++++++ .../template/gateway/service/UserService.kt | 33 +++++++---- 5 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/Role.kt create mode 100644 gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt diff --git a/common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt b/common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt index 534e0d0..ac75aa6 100644 --- a/common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt +++ b/common/src/jvmMain/kotlin/com/saveourtool/template/util/ReactorUtils.kt @@ -6,8 +6,14 @@ package com.saveourtool.template.util import org.springframework.http.HttpStatus import org.springframework.web.server.ResponseStatusException +import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import reactor.core.scheduler.Scheduler +import reactor.core.scheduler.Schedulers import reactor.kotlin.core.publisher.switchIfEmpty +import reactor.kotlin.core.publisher.toMono + +private val ioScheduler: Scheduler = Schedulers.boundedElastic() /** * @param status @@ -17,3 +23,20 @@ import reactor.kotlin.core.publisher.switchIfEmpty fun Mono.switchIfEmptyToResponseException(status: HttpStatus, messageCreator: (() -> String?) = { null }) = switchIfEmpty { Mono.error(ResponseStatusException(status, messageCreator())) } + + +/** + * Taking from https://projectreactor.io/docs/core/release/reference/#faq.wrap-blocking + * + * @param supplier blocking operation like JDBC + * @return [Mono] from result of blocking operation [T] + * @see blockingToFlux + */ +fun blockingToMono(supplier: () -> T?): Mono = supplier.toMono().subscribeOn(ioScheduler) + +/** + * @param supplier blocking operation like JDBC + * @return [Flux] from result of blocking operation [List] of [T] + * @see blockingToMono + */ +fun blockingToFlux(supplier: () -> Iterable): Flux = blockingToMono(supplier).flatMapIterable { it } diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/Role.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/Role.kt new file mode 100644 index 0000000..60e5d35 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/Role.kt @@ -0,0 +1,45 @@ +package com.saveourtool.template.gateway.entities + +/** + * User roles + * @property formattedName string representation of the [Role] that should be printed + * @property priority + */ +enum class Role( + val formattedName: String, + private val priority: Int, +) { + /** + * Has no role (synonym to null) + */ + NONE("None", 0), + + /** + * Has readonly access to public projects. + */ + VIEWER("Viewer", 1), + + /** + * admin in organization + */ + ADMIN("Admin", 2), + + /** + * User that has created this project + */ + OWNER("Owner", 3), + ; + + /** + * @return this role with default prefix for spring-security + */ + fun asSpringSecurityRole() = "ROLE_$name" + + companion object { + /** + * @param springSecurityRole + * @return [Role] found by [springSecurityRole] using [asSpringSecurityRole] + */ + fun fromSpringSecurityRole(springSecurityRole: String): Role? = entries.find { it.asSpringSecurityRole() == springSecurityRole } + } +} \ No newline at end of file diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt index 538e689..61b79e8 100644 --- a/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt @@ -1,9 +1,11 @@ package com.saveourtool.template.gateway.entities +import com.saveourtool.template.authentication.AppUserDetails import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue import jakarta.persistence.GenerationType import jakarta.persistence.Id +import org.springframework.security.core.userdetails.UserDetails /** * @property name @@ -20,4 +22,8 @@ class User( var password: String?, var role: String?, var email: String? = null, -) +) { + fun toUserDetails(): UserDetails { + AppUserDetails + } +} diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt new file mode 100644 index 0000000..e73b207 --- /dev/null +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + */ + +package com.saveourtool.template.gateway.security + +import com.saveourtool.template.gateway.service.UserService +import com.saveourtool.template.util.blockingToMono +import org.springframework.context.annotation.Bean +import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler + +/** + * @since 2024-03-20 + */ +@EnableWebFluxSecurity +class WebSecurityConfig( + private val userService: UserService, +) { + @Bean + fun securityWebFilterChain( + http: ServerHttpSecurity + ): SecurityWebFilterChain = http + .oauth2Login { + it.authenticationSuccessHandler( + DelegatingServerAuthenticationSuccessHandler( + StoringServerAuthenticationSuccessHandler(backendService), + RedirectServerAuthenticationSuccessHandler("/"), + ) + ) + it.authenticationFailureHandler( + RedirectServerAuthenticationFailureHandler("/error") + ) + } + .httpBasic { httpBasicSpec -> + // Authenticate by comparing received basic credentials with existing one from DB + httpBasicSpec.authenticationManager( + UserDetailsRepositoryReactiveAuthenticationManager { username -> + blockingToMono { + userService.findByName(username) + } + .filter { it.password != null } + .map { + + } + + backendService.findByName(username).cast() + } + ) + } + .build() +} \ No newline at end of file diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt index d68fa4d..9b75aaf 100644 --- a/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/service/UserService.kt @@ -1,5 +1,7 @@ package com.saveourtool.template.gateway.service +import com.saveourtool.template.gateway.entities.OriginalLogin +import com.saveourtool.template.gateway.entities.Role import com.saveourtool.template.gateway.entities.User import com.saveourtool.template.gateway.repository.OriginalLoginRepository import com.saveourtool.template.gateway.repository.UserRepository @@ -14,11 +16,10 @@ open class UserService( ) { /** * @param username - * @param source source (where the user identity is coming from) * @return existed [User] */ - fun findByOriginalLogin(username: String, source: String): User? = - originalLoginRepository.findByNameAndSource(username, source)?.user + fun findByName(username: String): User? = + userRepository.findByName(username) /** * @param source source (where the user identity is coming from) @@ -29,14 +30,22 @@ open class UserService( open fun getOrCreateNew( source: String, nameInSource: String, - ): User { - originalLoginRepository.findByNameAndSource(nameInSource, source) - ?.user - ?: run { - val newUser = User( - + ): User = originalLoginRepository.findByNameAndSource(nameInSource, source) + ?.user + ?: run { + val newUser = User( + name = nameInSource, + password = null, + role = Role.VIEWER.formattedName, + ) + .let { userRepository.save(it) } + originalLoginRepository.save( + OriginalLogin( + name = nameInSource, + source = source, + user = newUser ) - userRepository.save(newUser) - } - } + ) + newUser + } } \ No newline at end of file From 2e83f4cc0959a7c3f1953b31c35f8ae48e6fcddf Mon Sep 17 00:00:00 2001 From: Nariman Abdullin Date: Wed, 20 Mar 2024 18:29:36 +0300 Subject: [PATCH 4/5] WIP 3 --- .../template/gateway/entities/User.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt index 61b79e8..e29a0ec 100644 --- a/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/entities/User.kt @@ -20,10 +20,23 @@ class User( var id: Long? = null, var name: String, var password: String?, - var role: String?, + var role: String, var email: String? = null, ) { - fun toUserDetails(): UserDetails { - AppUserDetails + /** + * @return [id] as not null with validating + * @throws IllegalArgumentException when [id] is not set that means entity is not saved yet + */ + fun requiredId(): Long = requireNotNull(id) { + "Entity is not saved yet: $this" } + + /** + * @return + */ + fun toUserDetails(): UserDetails = AppUserDetails( + requiredId(), + name, + role, + ) } From 29e15fc121bccd798dcebd599fee8e5c841f4dff Mon Sep 17 00:00:00 2001 From: Nariman Abdullin Date: Mon, 15 Apr 2024 10:42:02 +0300 Subject: [PATCH 5/5] WIP --- .../authentication-service/build.gradle.kts | 21 +++++++++ .../src/db/db.changelog.xml | 0 .../src/db/original-login.xml | 0 .../authentication-service}/src/db/user.xml | 0 .../AuthenticationApplication.kt | 11 +++++ .../authentication/entities/OriginalLogin.kt | 25 +++++++++++ .../template/authentication/entities/Role.kt | 45 +++++++++++++++++++ .../template/authentication/entities/User.kt | 42 +++++++++++++++++ .../repository/OriginalLoginRepository.kt | 23 ++++++++++ .../repository/UserRepository.kt | 17 +++++++ .../authentication-utils}/build.gradle.kts | 0 .../template/authentication/AppUserDetails.kt | 0 gateway/build.gradle.kts | 2 +- .../gateway/security/WebSecurityConfig.kt | 1 + settings.gradle.kts | 3 +- 15 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 authentication/authentication-service/build.gradle.kts rename {gateway => authentication/authentication-service}/src/db/db.changelog.xml (100%) rename {gateway => authentication/authentication-service}/src/db/original-login.xml (100%) rename {gateway => authentication/authentication-service}/src/db/user.xml (100%) create mode 100644 authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/AuthenticationApplication.kt create mode 100644 authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/OriginalLogin.kt create mode 100644 authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/Role.kt create mode 100644 authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/User.kt create mode 100644 authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/OriginalLoginRepository.kt create mode 100644 authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/UserRepository.kt rename {authentication-utils => authentication/authentication-utils}/build.gradle.kts (100%) rename {authentication-utils => authentication/authentication-utils}/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt (100%) diff --git a/authentication/authentication-service/build.gradle.kts b/authentication/authentication-service/build.gradle.kts new file mode 100644 index 0000000..a3df7f3 --- /dev/null +++ b/authentication/authentication-service/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("com.saveourtool.template.build.spring-boot-kotlin-configuration") + id("com.saveourtool.template.build.mysql-local-run-configuration") +} + +dependencies { + implementation(projects.common) + implementation(projects.authentication.authenticationUtils) + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("org.springframework.cloud:spring-cloud-starter-gateway") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") + implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation(libs.kotlin.logging) +} + +mysqlLocalRun { + databaseName = "authentication" + liquibaseChangelogPath = project.layout.projectDirectory.file("src/db/db.changelog.xml") +} \ No newline at end of file diff --git a/gateway/src/db/db.changelog.xml b/authentication/authentication-service/src/db/db.changelog.xml similarity index 100% rename from gateway/src/db/db.changelog.xml rename to authentication/authentication-service/src/db/db.changelog.xml diff --git a/gateway/src/db/original-login.xml b/authentication/authentication-service/src/db/original-login.xml similarity index 100% rename from gateway/src/db/original-login.xml rename to authentication/authentication-service/src/db/original-login.xml diff --git a/gateway/src/db/user.xml b/authentication/authentication-service/src/db/user.xml similarity index 100% rename from gateway/src/db/user.xml rename to authentication/authentication-service/src/db/user.xml diff --git a/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/AuthenticationApplication.kt b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/AuthenticationApplication.kt new file mode 100644 index 0000000..78b39f3 --- /dev/null +++ b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/AuthenticationApplication.kt @@ -0,0 +1,11 @@ +package com.saveourtool.template.authentication + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class AuthenticationApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/OriginalLogin.kt b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/OriginalLogin.kt new file mode 100644 index 0000000..4e99961 --- /dev/null +++ b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/OriginalLogin.kt @@ -0,0 +1,25 @@ +package com.saveourtool.template.authentication.entities + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +/** + * @property name + * @property user + * @property source + */ +@Entity +class OriginalLogin( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var source: String, + @ManyToOne + @JoinColumn(name = "user_id") + var user: User, +) diff --git a/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/Role.kt b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/Role.kt new file mode 100644 index 0000000..fd9eea8 --- /dev/null +++ b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/Role.kt @@ -0,0 +1,45 @@ +package com.saveourtool.template.authentication.entities + +/** + * User roles + * @property formattedName string representation of the [Role] that should be printed + * @property priority + */ +enum class Role( + val formattedName: String, + private val priority: Int, +) { + /** + * Has no role (synonym to null) + */ + NONE("None", 0), + + /** + * Has readonly access to public projects. + */ + VIEWER("Viewer", 1), + + /** + * admin in organization + */ + ADMIN("Admin", 2), + + /** + * User that has created this project + */ + OWNER("Owner", 3), + ; + + /** + * @return this role with default prefix for spring-security + */ + fun asSpringSecurityRole() = "ROLE_$name" + + companion object { + /** + * @param springSecurityRole + * @return [Role] found by [springSecurityRole] using [asSpringSecurityRole] + */ + fun fromSpringSecurityRole(springSecurityRole: String): Role? = entries.find { it.asSpringSecurityRole() == springSecurityRole } + } +} \ No newline at end of file diff --git a/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/User.kt b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/User.kt new file mode 100644 index 0000000..300c49d --- /dev/null +++ b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/entities/User.kt @@ -0,0 +1,42 @@ +package com.saveourtool.template.authentication.entities + +import com.saveourtool.template.authentication.AppUserDetails +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import org.springframework.security.core.userdetails.UserDetails + +/** + * @property name + * @property password *in plain text* + * @property role role of this user + * @property email email of user + */ +@Entity +class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var password: String?, + var role: String, + var email: String? = null, +) { + /** + * @return [id] as not null with validating + * @throws IllegalArgumentException when [id] is not set that means entity is not saved yet + */ + fun requiredId(): Long = requireNotNull(id) { + "Entity is not saved yet: $this" + } + + /** + * @return + */ + fun toUserDetails(): UserDetails = AppUserDetails( + requiredId(), + name, + role, + ) +} diff --git a/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/OriginalLoginRepository.kt b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/OriginalLoginRepository.kt new file mode 100644 index 0000000..5f2dcd0 --- /dev/null +++ b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/OriginalLoginRepository.kt @@ -0,0 +1,23 @@ +package com.saveourtool.template.gateway.repository + +import com.saveourtool.template.gateway.entities.OriginalLogin +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +/** + * Repository to access data about original user logins and sources + */ +@Repository +interface OriginalLoginRepository : JpaRepository{ + /** + * @param name + * @param source + * @return user or null if no results have been found + */ + fun findByNameAndSource(name: String, source: String): OriginalLogin? + + /** + * @param id id of user + */ + fun deleteByUserId(id: Long) +} diff --git a/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/UserRepository.kt b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/UserRepository.kt new file mode 100644 index 0000000..0835901 --- /dev/null +++ b/authentication/authentication-service/src/main/kotlin/com/saveourtool/template/authentication/repository/UserRepository.kt @@ -0,0 +1,17 @@ +package com.saveourtool.template.gateway.repository + +import com.saveourtool.template.gateway.entities.User +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +/** + * Repository to access data about users + */ +@Repository +interface UserRepository : JpaRepository { + /** + * @param username + * @return user or null if no results have been found + */ + fun findByName(username: String): User? +} diff --git a/authentication-utils/build.gradle.kts b/authentication/authentication-utils/build.gradle.kts similarity index 100% rename from authentication-utils/build.gradle.kts rename to authentication/authentication-utils/build.gradle.kts diff --git a/authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt b/authentication/authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt similarity index 100% rename from authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt rename to authentication/authentication-utils/src/main/kotlin/com/saveourtool/template/authentication/AppUserDetails.kt diff --git a/gateway/build.gradle.kts b/gateway/build.gradle.kts index 2e9208b..e5ebd7b 100644 --- a/gateway/build.gradle.kts +++ b/gateway/build.gradle.kts @@ -5,7 +5,7 @@ plugins { dependencies { implementation(projects.common) - implementation(projects.authenticationUtils) + implementation(projects.authentication.authenticationUtils) implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") implementation("org.springframework.cloud:spring-cloud-starter-gateway") diff --git a/gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt b/gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt index e73b207..06cbe13 100644 --- a/gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt +++ b/gateway/src/main/kotlin/com/saveourtool/template/gateway/security/WebSecurityConfig.kt @@ -5,6 +5,7 @@ package com.saveourtool.template.gateway.security import com.saveourtool.template.gateway.service.UserService +import com.saveourtool.template.gateway.utils.StoringServerAuthenticationSuccessHandler import com.saveourtool.template.util.blockingToMono import org.springframework.context.annotation.Bean import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager diff --git a/settings.gradle.kts b/settings.gradle.kts index 736b893..7240256 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,7 +24,8 @@ plugins { includeBuild("gradle/plugins") include("common") -include("authentication-utils") +include("authentication:authentication-service") +include("authentication:authentication-utils") include("backend-webmvc") include("backend-webflux") include("gateway")