(vision) SCS/DDD
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.util.pipeline.*
|
||||
import at.mocode.enums.RolleE
|
||||
import at.mocode.enums.BerechtigungE
|
||||
|
||||
/**
|
||||
* Authorization configuration and middleware for role-based access control.
|
||||
*
|
||||
* Provides utilities for checking user roles and permissions on protected endpoints.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum representing user roles in the system.
|
||||
*/
|
||||
enum class UserRole {
|
||||
ADMIN,
|
||||
VEREINS_ADMIN,
|
||||
FUNKTIONAER,
|
||||
REITER,
|
||||
TRAINER,
|
||||
RICHTER,
|
||||
TIERARZT,
|
||||
ZUSCHAUER,
|
||||
GAST
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing permissions in the system.
|
||||
*/
|
||||
enum class Permission {
|
||||
// Person management
|
||||
PERSON_READ,
|
||||
PERSON_CREATE,
|
||||
PERSON_UPDATE,
|
||||
PERSON_DELETE,
|
||||
|
||||
// Club management
|
||||
VEREIN_READ,
|
||||
VEREIN_CREATE,
|
||||
VEREIN_UPDATE,
|
||||
VEREIN_DELETE,
|
||||
|
||||
// Event management
|
||||
VERANSTALTUNG_READ,
|
||||
VERANSTALTUNG_CREATE,
|
||||
VERANSTALTUNG_UPDATE,
|
||||
VERANSTALTUNG_DELETE,
|
||||
|
||||
// Horse management
|
||||
PFERD_READ,
|
||||
PFERD_CREATE,
|
||||
PFERD_UPDATE,
|
||||
PFERD_DELETE,
|
||||
|
||||
// Master data management
|
||||
STAMMDATEN_READ,
|
||||
STAMMDATEN_UPDATE,
|
||||
|
||||
// System administration
|
||||
SYSTEM_ADMIN,
|
||||
BENUTZER_VERWALTEN,
|
||||
ROLLEN_VERWALTEN
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing user authorization context.
|
||||
*/
|
||||
data class UserAuthContext(
|
||||
val userId: String,
|
||||
val username: String,
|
||||
val roles: List<UserRole>,
|
||||
val permissions: List<Permission>
|
||||
)
|
||||
|
||||
/**
|
||||
* Maps domain role enum to authorization role enum.
|
||||
*/
|
||||
private fun mapDomainRoleToUserRole(domainRole: RolleE): UserRole {
|
||||
return when (domainRole) {
|
||||
RolleE.ADMIN -> UserRole.ADMIN
|
||||
RolleE.VEREINS_ADMIN -> UserRole.VEREINS_ADMIN
|
||||
RolleE.FUNKTIONAER -> UserRole.FUNKTIONAER
|
||||
RolleE.REITER -> UserRole.REITER
|
||||
RolleE.TRAINER -> UserRole.TRAINER
|
||||
RolleE.RICHTER -> UserRole.RICHTER
|
||||
RolleE.TIERARZT -> UserRole.TIERARZT
|
||||
RolleE.ZUSCHAUER -> UserRole.ZUSCHAUER
|
||||
RolleE.GAST -> UserRole.GAST
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps domain permission enum to authorization permission enum.
|
||||
*/
|
||||
private fun mapDomainPermissionToPermission(domainPermission: BerechtigungE): Permission {
|
||||
return when (domainPermission) {
|
||||
BerechtigungE.PERSON_READ -> Permission.PERSON_READ
|
||||
BerechtigungE.PERSON_CREATE -> Permission.PERSON_CREATE
|
||||
BerechtigungE.PERSON_UPDATE -> Permission.PERSON_UPDATE
|
||||
BerechtigungE.PERSON_DELETE -> Permission.PERSON_DELETE
|
||||
BerechtigungE.VEREIN_READ -> Permission.VEREIN_READ
|
||||
BerechtigungE.VEREIN_CREATE -> Permission.VEREIN_CREATE
|
||||
BerechtigungE.VEREIN_UPDATE -> Permission.VEREIN_UPDATE
|
||||
BerechtigungE.VEREIN_DELETE -> Permission.VEREIN_DELETE
|
||||
BerechtigungE.VERANSTALTUNG_READ -> Permission.VERANSTALTUNG_READ
|
||||
BerechtigungE.VERANSTALTUNG_CREATE -> Permission.VERANSTALTUNG_CREATE
|
||||
BerechtigungE.VERANSTALTUNG_UPDATE -> Permission.VERANSTALTUNG_UPDATE
|
||||
BerechtigungE.VERANSTALTUNG_DELETE -> Permission.VERANSTALTUNG_DELETE
|
||||
BerechtigungE.PFERD_READ -> Permission.PFERD_READ
|
||||
BerechtigungE.PFERD_CREATE -> Permission.PFERD_CREATE
|
||||
BerechtigungE.PFERD_UPDATE -> Permission.PFERD_UPDATE
|
||||
BerechtigungE.PFERD_DELETE -> Permission.PFERD_DELETE
|
||||
BerechtigungE.STAMMDATEN_READ -> Permission.STAMMDATEN_READ
|
||||
BerechtigungE.STAMMDATEN_UPDATE -> Permission.STAMMDATEN_UPDATE
|
||||
BerechtigungE.SYSTEM_ADMIN -> Permission.SYSTEM_ADMIN
|
||||
BerechtigungE.BENUTZER_VERWALTEN -> Permission.BENUTZER_VERWALTEN
|
||||
BerechtigungE.ROLLEN_VERWALTEN -> Permission.ROLLEN_VERWALTEN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to get user authorization context from JWT principal.
|
||||
*/
|
||||
fun JWTPrincipal.getUserAuthContext(): UserAuthContext? {
|
||||
val userId = getClaim("userId", String::class) ?: return null
|
||||
val username = getClaim("username", String::class) ?: return null
|
||||
|
||||
// Get roles and permissions from JWT token
|
||||
val domainRoles = getClaim("roles", Array<RolleE>::class)?.toList() ?: emptyList()
|
||||
val domainPermissions = getClaim("permissions", Array<BerechtigungE>::class)?.toList() ?: emptyList()
|
||||
|
||||
// Map domain enums to authorization enums
|
||||
val roles = domainRoles.map { mapDomainRoleToUserRole(it) }
|
||||
val permissions = domainPermissions.map { mapDomainPermissionToPermission(it) }
|
||||
|
||||
return UserAuthContext(
|
||||
userId = userId,
|
||||
username = username,
|
||||
roles = roles,
|
||||
permissions = permissions
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps roles to their corresponding permissions.
|
||||
*/
|
||||
private fun getRolePermissions(roles: List<UserRole>): List<Permission> {
|
||||
val permissions = mutableSetOf<Permission>()
|
||||
|
||||
roles.forEach { role ->
|
||||
when (role) {
|
||||
UserRole.ADMIN -> {
|
||||
permissions.addAll(Permission.values())
|
||||
}
|
||||
UserRole.VEREINS_ADMIN -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ, Permission.PERSON_CREATE, Permission.PERSON_UPDATE,
|
||||
Permission.VEREIN_READ, Permission.VEREIN_UPDATE,
|
||||
Permission.PFERD_READ, Permission.PFERD_CREATE, Permission.PFERD_UPDATE,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.FUNKTIONAER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ, Permission.VERANSTALTUNG_CREATE, Permission.VERANSTALTUNG_UPDATE,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.TRAINER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.REITER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.RICHTER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.TIERARZT -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.ZUSCHAUER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.GAST -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return permissions.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Route extension function to require specific roles.
|
||||
*/
|
||||
fun Route.requireRoles(vararg roles: UserRole, build: Route.() -> Unit): Route {
|
||||
val route = createChild(object : RouteSelector() {
|
||||
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
|
||||
return RouteSelectorEvaluation.Constant
|
||||
}
|
||||
|
||||
override fun toString(): String = "requireRoles(${roles.joinToString()})"
|
||||
})
|
||||
|
||||
route.intercept(ApplicationCallPipeline.Call) {
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val authContext = principal?.getUserAuthContext()
|
||||
|
||||
if (authContext == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
|
||||
val hasRequiredRole = roles.any { requiredRole ->
|
||||
authContext.roles.contains(requiredRole)
|
||||
}
|
||||
|
||||
if (!hasRequiredRole) {
|
||||
call.respond(
|
||||
HttpStatusCode.Forbidden,
|
||||
"Access denied. Required roles: ${roles.joinToString()}"
|
||||
)
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
}
|
||||
|
||||
route.build()
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Route extension function to require specific permissions.
|
||||
*/
|
||||
fun Route.requirePermissions(vararg permissions: Permission, build: Route.() -> Unit): Route {
|
||||
val route = createChild(object : RouteSelector() {
|
||||
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
|
||||
return RouteSelectorEvaluation.Constant
|
||||
}
|
||||
|
||||
override fun toString(): String = "requirePermissions(${permissions.joinToString()})"
|
||||
})
|
||||
|
||||
route.intercept(ApplicationCallPipeline.Call) {
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val authContext = principal?.getUserAuthContext()
|
||||
|
||||
if (authContext == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
|
||||
val hasAllPermissions = permissions.all { requiredPermission ->
|
||||
authContext.permissions.contains(requiredPermission)
|
||||
}
|
||||
|
||||
if (!hasAllPermissions) {
|
||||
call.respond(
|
||||
HttpStatusCode.Forbidden,
|
||||
"Access denied. Required permissions: ${permissions.joinToString()}"
|
||||
)
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
}
|
||||
|
||||
route.build()
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline context extension to get current user authorization context.
|
||||
*/
|
||||
val PipelineContext<Unit, ApplicationCall>.userAuthContext: UserAuthContext?
|
||||
get() = call.principal<JWTPrincipal>()?.getUserAuthContext()
|
||||
|
||||
/**
|
||||
* Application call extension to check if user has specific role.
|
||||
*/
|
||||
fun ApplicationCall.hasRole(role: UserRole): Boolean {
|
||||
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
|
||||
return authContext?.roles?.contains(role) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Application call extension to check if user has specific permission.
|
||||
*/
|
||||
fun ApplicationCall.hasPermission(permission: Permission): Boolean {
|
||||
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
|
||||
return authContext?.permissions?.contains(permission) == true
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import io.ktor.server.application.*
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Database configuration for the API Gateway.
|
||||
@@ -11,6 +12,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
||||
* Sets up database connections and schema initialization for all bounded contexts.
|
||||
*/
|
||||
fun Application.configureDatabase() {
|
||||
val log = LoggerFactory.getLogger("DatabaseConfig")
|
||||
val databaseUrl = environment.config.propertyOrNull("database.url")?.getString()
|
||||
?: "jdbc:postgresql://localhost:5432/meldestelle"
|
||||
val databaseUser = environment.config.propertyOrNull("database.user")?.getString()
|
||||
@@ -46,6 +48,11 @@ fun Application.configureDatabase() {
|
||||
at.mocode.horses.infrastructure.repository.HorseTable
|
||||
)
|
||||
|
||||
// Event Management Context tables
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
at.mocode.events.infrastructure.repository.VeranstaltungTable
|
||||
)
|
||||
|
||||
log.info("Database schemas initialized successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize database schemas: ${e.message}")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.openapi.*
|
||||
import io.ktor.server.plugins.swagger.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
/**
|
||||
* Configuration for OpenAPI/Swagger documentation.
|
||||
*
|
||||
* This module configures the OpenAPI specification generation and Swagger UI
|
||||
* for the API Gateway, providing comprehensive API documentation.
|
||||
*/
|
||||
fun Application.configureOpenApi() {
|
||||
install(OpenAPI) {
|
||||
codegen = org.openapitools.codegen.CodegenType.CLIENT
|
||||
info {
|
||||
title = "Meldestelle Self-Contained Systems API"
|
||||
version = "1.0.0"
|
||||
description = "Unified API Gateway for Austrian Equestrian Federation bounded contexts"
|
||||
contact {
|
||||
name = "API Support"
|
||||
email = "support@mocode.at"
|
||||
}
|
||||
license {
|
||||
name = "MIT"
|
||||
url = "https://opensource.org/licenses/MIT"
|
||||
}
|
||||
}
|
||||
server("http://localhost:8080") {
|
||||
description = "Development server"
|
||||
}
|
||||
server("https://api.meldestelle.at") {
|
||||
description = "Production server"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Swagger UI.
|
||||
*
|
||||
* Provides an interactive web interface for exploring and testing the API.
|
||||
*/
|
||||
fun Application.configureSwagger() {
|
||||
routing {
|
||||
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml") {
|
||||
version = "4.15.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,16 @@ package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.http.*
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
|
||||
/**
|
||||
* Security configuration for the API Gateway.
|
||||
*
|
||||
* Configures CORS, authentication, and other security-related settings.
|
||||
* Configures CORS, JWT authentication, and other security-related settings.
|
||||
*/
|
||||
fun Application.configureSecurity() {
|
||||
install(CORS) {
|
||||
@@ -29,10 +33,52 @@ fun Application.configureSecurity() {
|
||||
anyHost() // This should be restricted in production
|
||||
}
|
||||
|
||||
// TODO: Add JWT authentication configuration
|
||||
// install(Authentication) {
|
||||
// jwt("auth-jwt") {
|
||||
// // JWT configuration
|
||||
// }
|
||||
// }
|
||||
// JWT Configuration
|
||||
val jwtConfig = JwtConfig.fromEnvironment()
|
||||
|
||||
install(Authentication) {
|
||||
jwt("auth-jwt") {
|
||||
realm = jwtConfig.realm
|
||||
verifier(
|
||||
JWT
|
||||
.require(Algorithm.HMAC256(jwtConfig.secret))
|
||||
.withAudience(jwtConfig.audience)
|
||||
.withIssuer(jwtConfig.issuer)
|
||||
.build()
|
||||
)
|
||||
validate { credential ->
|
||||
if (credential.payload.getClaim("userId").asString() != null) {
|
||||
JWTPrincipal(credential.payload)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { defaultScheme, realm ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT Configuration data class.
|
||||
*/
|
||||
data class JwtConfig(
|
||||
val secret: String,
|
||||
val issuer: String,
|
||||
val audience: String,
|
||||
val realm: String,
|
||||
val expirationTime: Long = 3600000L // 1 hour in milliseconds
|
||||
) {
|
||||
companion object {
|
||||
fun fromEnvironment(): JwtConfig {
|
||||
return JwtConfig(
|
||||
secret = System.getenv("JWT_SECRET") ?: "default-secret-key-change-in-production",
|
||||
issuer = System.getenv("JWT_ISSUER") ?: "meldestelle-api",
|
||||
audience = System.getenv("JWT_AUDIENCE") ?: "meldestelle-users",
|
||||
realm = System.getenv("JWT_REALM") ?: "Meldestelle API",
|
||||
expirationTime = System.getenv("JWT_EXPIRATION")?.toLongOrNull() ?: 3600000L
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user