Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gateway #22

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions authentication/authentication-service/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
11 changes: 11 additions & 0 deletions authentication/authentication-service/src/db/db.changelog.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

<include file="user.xml" relativeToChangelogFile="true"/>
<include file="original-login.xml" relativeToChangelogFile="true"/>

</databaseChangeLog>
27 changes: 27 additions & 0 deletions authentication/authentication-service/src/db/original-login.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

<changeSet id="original-login-creation" author="saveourtool-dev">
<createTable tableName="original_login">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="user_id" type="bigint">
<constraints foreignKeyName="fk_original_login_user" references="user(id)"
nullable="false" deleteCascade="true"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="source" type="varchar(64)">
<constraints nullable="false"/>
</column>
</createTable>
<addUniqueConstraint tableName="original_login" columnNames="name,source"/>
</changeSet>

</databaseChangeLog>
23 changes: 23 additions & 0 deletions authentication/authentication-service/src/db/user.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

<changeSet id="user-creation" author="saveourtool-dev">
<createTable tableName="user">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="password" type="varchar(256)"/>
<column name="role" type="varchar(64)"/>
<column name="email" type="varchar(250)"/>
</createTable>
<addUniqueConstraint tableName="user" constraintName="uq_user_name" columnNames="name"/>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -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<String>) {
runApplication<AuthenticationApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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<OriginalLogin, Long>{
/**
* @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)
}
Original file line number Diff line number Diff line change
@@ -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<User, Long> {
/**
* @param username
* @return user or null if no results have been found
*/
fun findByName(username: String): User?
}
9 changes: 9 additions & 0 deletions authentication/authentication-utils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<out GrantedAuthority> = 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 <T> logWarnAndReturnEmpty(missedHeaderName: String): T? {
log.debug {
"Header $missedHeaderName is not provided: skipping pre-authenticated save-user authentication"
}
return null
}
}
}
5 changes: 2 additions & 3 deletions backend-webflux/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
5 changes: 2 additions & 3 deletions backend-webmvc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
Loading