mirror of
https://github.com/2martens/tsw-timetable.git
synced 2026-10-17 16:06:51 +02:00
security: Users can only access their own resources
fix: Make sure JWTs from Keycloak are understood and CORS is respected
This commit is contained in:
@@ -126,8 +126,6 @@ spring-boot-server = [
|
|||||||
"spring-ui",
|
"spring-ui",
|
||||||
]
|
]
|
||||||
spring-boot-security = [
|
spring-boot-security = [
|
||||||
"keycloak-core",
|
|
||||||
"keycloak-policy-enforcer",
|
|
||||||
"spring-boot-oauth2-resource-server",
|
"spring-boot-oauth2-resource-server",
|
||||||
"spring-boot-security",
|
"spring-boot-security",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import io.swagger.v3.oas.annotations.tags.Tag
|
|||||||
import org.mapstruct.factory.Mappers
|
import org.mapstruct.factory.Mappers
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.authorization.AuthorizationDeniedException
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -60,6 +63,9 @@ class UserController(
|
|||||||
)
|
)
|
||||||
]) body: User
|
]) body: User
|
||||||
): ResponseEntity<User> {
|
): ResponseEntity<User> {
|
||||||
|
val authentication = SecurityContextHolder.getContext().authentication
|
||||||
|
val jwt = authentication as Jwt
|
||||||
|
val authorizedParty = jwt.getClaimAsString("azp")
|
||||||
var created = false
|
var created = false
|
||||||
|
|
||||||
val userIdConverted = UserId.of(NonEmptyString(userId))
|
val userIdConverted = UserId.of(NonEmptyString(userId))
|
||||||
@@ -67,9 +73,11 @@ class UserController(
|
|||||||
if (user == null) {
|
if (user == null) {
|
||||||
created = true
|
created = true
|
||||||
user = mapper.mapToDB(body)
|
user = mapper.mapToDB(body)
|
||||||
} else {
|
} else if (authorizedParty == userId) {
|
||||||
user.name = NonEmptyString(body.name)
|
user.name = NonEmptyString(body.name)
|
||||||
user.email = Email.of(NonEmptyString(body.email))
|
user.email = Email.of(NonEmptyString(body.email))
|
||||||
|
} else {
|
||||||
|
throw AuthorizationDeniedException("The user is not authorized to update this user")
|
||||||
}
|
}
|
||||||
|
|
||||||
userRepository.save(user)
|
userRepository.save(user)
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package de.twomartens.timetable.configuration
|
||||||
|
|
||||||
|
import de.twomartens.timetable.configuration.roles.CheckParty
|
||||||
|
import de.twomartens.timetable.configuration.roles.PartyAuthorizationManager
|
||||||
|
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.context.annotation.Role
|
||||||
|
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder
|
||||||
|
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class AuthorizationManagerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Role(ROLE_INFRASTRUCTURE)
|
||||||
|
fun checkPartyInterceptor(): AuthorizationManagerBeforeMethodInterceptor {
|
||||||
|
val pointcut = AnnotationMatchingPointcut(null, CheckParty::class.java, true)
|
||||||
|
val interceptor = AuthorizationManagerBeforeMethodInterceptor(pointcut, PartyAuthorizationManager())
|
||||||
|
interceptor.order = AuthorizationInterceptorsOrder.PRE_AUTHORIZE.order
|
||||||
|
return interceptor
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package de.twomartens.timetable.configuration
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.web.cors.CorsConfiguration
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class CorsConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Qualifier("corsApplication")
|
||||||
|
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||||
|
val configuration = CorsConfiguration()
|
||||||
|
configuration.allowedOrigins = listOf("http://localhost:8100", "https://timetable.2martens.de") // frontend origin
|
||||||
|
configuration.allowedMethods = listOf("GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS")
|
||||||
|
configuration.allowedHeaders = listOf("*")
|
||||||
|
configuration.allowCredentials = true
|
||||||
|
|
||||||
|
val source = UrlBasedCorsConfigurationSource()
|
||||||
|
source.registerCorsConfiguration("/**", configuration)
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-59
@@ -1,73 +1,36 @@
|
|||||||
package de.twomartens.timetable.configuration
|
package de.twomartens.timetable.configuration
|
||||||
|
|
||||||
import de.twomartens.support.security.SpringPolicyEnforcerFilter
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.keycloak.adapters.authorization.spi.ConfigurationResolver
|
|
||||||
import org.keycloak.adapters.authorization.spi.HttpRequest
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig
|
|
||||||
import org.keycloak.util.JsonSerialization
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.security.config.Customizer
|
import org.springframework.security.config.Customizer
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
|
import org.springframework.security.config.http.SessionCreationPolicy
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder
|
|
||||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
|
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
import java.io.IOException
|
import org.springframework.web.cors.CorsConfigurationSource
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
open class WebSecurityConfiguration {
|
open class WebSecurityConfiguration {
|
||||||
@Value("\${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
|
|
||||||
private lateinit var jwkSetUri: String
|
|
||||||
|
|
||||||
@Value("#{environment.CLIENT_SECRET}")
|
|
||||||
private lateinit var clientSecret: String
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
open fun securityFilterChain(http: HttpSecurity,
|
||||||
|
@Qualifier("corsApplication") corsConfiguration: CorsConfigurationSource): SecurityFilterChain {
|
||||||
http
|
http
|
||||||
|
.cors { it.configurationSource(corsConfiguration) }
|
||||||
.csrf { it.disable() }
|
.csrf { it.disable() }
|
||||||
|
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||||
.authorizeHttpRequests { it.requestMatchers(*PERMITTED_PATHS.toTypedArray<String>()).permitAll() }
|
.authorizeHttpRequests { it.requestMatchers(*PERMITTED_PATHS.toTypedArray<String>()).permitAll() }
|
||||||
.authorizeHttpRequests { it.requestMatchers(HttpMethod.OPTIONS).permitAll() }
|
.authorizeHttpRequests { it.requestMatchers(HttpMethod.OPTIONS).permitAll() }
|
||||||
.authorizeHttpRequests { it.anyRequest().authenticated() }
|
.authorizeHttpRequests { it.anyRequest().authenticated() }
|
||||||
.oauth2ResourceServer { obj: OAuth2ResourceServerConfigurer<HttpSecurity?> -> obj.jwt(Customizer.withDefaults()) }
|
.oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
open fun jwtDecoder(): JwtDecoder {
|
|
||||||
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPolicyEnforcerFilter(): SpringPolicyEnforcerFilter {
|
|
||||||
return SpringPolicyEnforcerFilter(object : ConfigurationResolver {
|
|
||||||
override fun resolve(request: HttpRequest): PolicyEnforcerConfig {
|
|
||||||
return try {
|
|
||||||
val policyEnforcerConfig = JsonSerialization.readValue(
|
|
||||||
javaClass.getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig::class.java
|
|
||||||
)
|
|
||||||
policyEnforcerConfig.credentials = mapOf(Pair("secret", clientSecret))
|
|
||||||
if (request.method == HttpMethod.OPTIONS.name()) {
|
|
||||||
// always allow options request
|
|
||||||
policyEnforcerConfig.enforcementMode = EnforcementMode.DISABLED
|
|
||||||
} else {
|
|
||||||
policyEnforcerConfig.paths = PATHS
|
|
||||||
}
|
|
||||||
policyEnforcerConfig
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val PERMITTED_PATHS: Collection<String> = listOf(
|
private val PERMITTED_PATHS: Collection<String> = listOf(
|
||||||
"/timetable/healthCheck",
|
"/timetable/healthCheck",
|
||||||
@@ -77,17 +40,5 @@ open class WebSecurityConfiguration {
|
|||||||
"/error",
|
"/error",
|
||||||
"/timetable/version",
|
"/timetable/version",
|
||||||
)
|
)
|
||||||
private val PATHS = buildPathConfigs()
|
|
||||||
|
|
||||||
private fun buildPathConfigs(): List<PathConfig> {
|
|
||||||
val paths: MutableList<PathConfig> = mutableListOf()
|
|
||||||
for (path in PERMITTED_PATHS) {
|
|
||||||
val pathConfig = PathConfig()
|
|
||||||
pathConfig.path = path.replace("**", "*")
|
|
||||||
pathConfig.enforcementMode = EnforcementMode.DISABLED
|
|
||||||
paths.add(pathConfig)
|
|
||||||
}
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package de.twomartens.timetable.configuration.roles
|
||||||
|
|
||||||
|
import java.lang.annotation.Inherited
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Inherited
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class CheckParty(
|
||||||
|
val partyParamName: String // Name of method argument holding the party ID
|
||||||
|
)
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package de.twomartens.timetable.configuration.roles
|
||||||
|
|
||||||
|
import org.aopalliance.intercept.MethodInvocation
|
||||||
|
import org.springframework.security.authorization.AuthorizationDecision
|
||||||
|
import org.springframework.security.authorization.AuthorizationDeniedException
|
||||||
|
import org.springframework.security.authorization.AuthorizationManager
|
||||||
|
import org.springframework.security.authorization.AuthorizationResult
|
||||||
|
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler
|
||||||
|
import org.springframework.security.core.Authentication
|
||||||
|
import java.util.function.Supplier
|
||||||
|
|
||||||
|
class PartyAuthorizationManager : AuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
|
||||||
|
@Deprecated("Deprecated in Spring Security")
|
||||||
|
override fun check(authentication: Supplier<Authentication?>?, invocation: MethodInvocation?): AuthorizationDecision {
|
||||||
|
if (authentication == null || authentication.get() == null || !authentication.get()!!.isAuthenticated) {
|
||||||
|
return AuthorizationDecision(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val annotation = invocation!!.method.getAnnotation(CheckParty::class.java)
|
||||||
|
?: invocation.method.declaringClass.getAnnotation(CheckParty::class.java)
|
||||||
|
?: return AuthorizationDecision(true) // No annotation, allow
|
||||||
|
|
||||||
|
val authentication = authentication.get()!!
|
||||||
|
val partyParamName = annotation.partyParamName
|
||||||
|
|
||||||
|
// 1. Authorized party check
|
||||||
|
val methodParams = invocation.method.parameters
|
||||||
|
val argIndex = methodParams.indexOfFirst { it.name == partyParamName }
|
||||||
|
if (argIndex == -1) {
|
||||||
|
return AuthorizationDecision(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestedPartyId = invocation.arguments[argIndex]?.toString()
|
||||||
|
val tokenPartyId = (authentication.details as? Map<*, *>)?.get("azp")?.toString()
|
||||||
|
?: authentication.name // fallback
|
||||||
|
|
||||||
|
return AuthorizationDecision(requestedPartyId == tokenPartyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleDeniedInvocation(methodInvocation: MethodInvocation?, authorizationResult: AuthorizationResult?): Any? {
|
||||||
|
throw AuthorizationDeniedException("The requested resources are not accessible to the authorized party")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.twomartens.timetable.formation
|
package de.twomartens.timetable.formation
|
||||||
|
|
||||||
import de.twomartens.timetable.auth.UserRepository
|
import de.twomartens.timetable.auth.UserRepository
|
||||||
|
import de.twomartens.timetable.configuration.roles.CheckParty
|
||||||
import de.twomartens.timetable.model.common.FormationId
|
import de.twomartens.timetable.model.common.FormationId
|
||||||
import de.twomartens.timetable.model.common.UserId
|
import de.twomartens.timetable.model.common.UserId
|
||||||
import de.twomartens.timetable.model.dto.Formation
|
import de.twomartens.timetable.model.dto.Formation
|
||||||
@@ -44,6 +45,7 @@ class FormationController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@GetMapping("/{userId}/")
|
@GetMapping("/{userId}/")
|
||||||
fun getFormations(
|
fun getFormations(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
@@ -96,6 +98,7 @@ class FormationController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@GetMapping("/{userId}/{id}")
|
@GetMapping("/{userId}/{id}")
|
||||||
fun getFormation(
|
fun getFormation(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
@@ -144,6 +147,7 @@ class FormationController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@PutMapping("/{userId}/{id}")
|
@PutMapping("/{userId}/{id}")
|
||||||
fun putFormation(
|
fun putFormation(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
@@ -236,6 +240,7 @@ class FormationController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@DeleteMapping("/{userId}/{id}")
|
@DeleteMapping("/{userId}/{id}")
|
||||||
fun deleteFormation(
|
fun deleteFormation(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.twomartens.timetable.route
|
package de.twomartens.timetable.route
|
||||||
|
|
||||||
import de.twomartens.timetable.auth.UserRepository
|
import de.twomartens.timetable.auth.UserRepository
|
||||||
|
import de.twomartens.timetable.configuration.roles.CheckParty
|
||||||
import de.twomartens.timetable.model.common.RouteId
|
import de.twomartens.timetable.model.common.RouteId
|
||||||
import de.twomartens.timetable.model.common.UserId
|
import de.twomartens.timetable.model.common.UserId
|
||||||
import de.twomartens.timetable.model.dto.TswRoute
|
import de.twomartens.timetable.model.dto.TswRoute
|
||||||
@@ -43,6 +44,7 @@ class RouteController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@GetMapping("/{userId}/")
|
@GetMapping("/{userId}/")
|
||||||
fun getRoutes(
|
fun getRoutes(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
@@ -87,6 +89,7 @@ class RouteController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@GetMapping("/{userId}/{id}")
|
@GetMapping("/{userId}/{id}")
|
||||||
fun getRoute(
|
fun getRoute(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
@@ -130,6 +133,7 @@ class RouteController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@PutMapping("/{userId}/{id}")
|
@PutMapping("/{userId}/{id}")
|
||||||
fun putRoute(
|
fun putRoute(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
@@ -195,6 +199,7 @@ class RouteController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@CheckParty(partyParamName = "userId")
|
||||||
@DeleteMapping("/{userId}/{id}")
|
@DeleteMapping("/{userId}/{id}")
|
||||||
fun deleteRoute(
|
fun deleteRoute(
|
||||||
@PathVariable @Parameter(description = "The id of the user",
|
@PathVariable @Parameter(description = "The id of the user",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag
|
import io.swagger.v3.oas.annotations.tags.Tag
|
||||||
import org.mapstruct.factory.Mappers
|
import org.mapstruct.factory.Mappers
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -87,6 +88,7 @@ class StationController(
|
|||||||
)
|
)
|
||||||
@SecurityRequirement(name = "bearer")
|
@SecurityRequirement(name = "bearer")
|
||||||
@SecurityRequirement(name = "oauth2")
|
@SecurityRequirement(name = "oauth2")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/update")
|
@PostMapping("/update")
|
||||||
fun updateStations(): ResponseEntity<Void> {
|
fun updateStations(): ResponseEntity<Void> {
|
||||||
val stations = bahnApiService.fetchStations("*")
|
val stations = bahnApiService.fetchStations("*")
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ spring:
|
|||||||
oauth2:
|
oauth2:
|
||||||
resourceserver:
|
resourceserver:
|
||||||
jwt:
|
jwt:
|
||||||
|
jws-algorithms: ES256
|
||||||
jwk-set-uri: ${KEYCLOAK_URL:https://id.2martens.de}/realms/${KEYCLOAK_REALM:twomartens}/protocol/openid-connect/certs
|
jwk-set-uri: ${KEYCLOAK_URL:https://id.2martens.de}/realms/${KEYCLOAK_REALM:twomartens}/protocol/openid-connect/certs
|
||||||
|
issuer-uri: ${KEYCLOAK_URL:https://id.2martens.de}/realms/${KEYCLOAK_REALM:twomartens}
|
||||||
application:
|
application:
|
||||||
name: timetable
|
name: timetable
|
||||||
|
|
||||||
@@ -79,10 +81,8 @@ de.twomartens.timetable:
|
|||||||
bahn-api:
|
bahn-api:
|
||||||
client-id: ${DB_CLIENT_ID}
|
client-id: ${DB_CLIENT_ID}
|
||||||
client-secret: ${DB_CLIENT_SECRET}
|
client-secret: ${DB_CLIENT_SECRET}
|
||||||
greeting: "Hello %s, on millenium"
|
|
||||||
statusprobe:
|
statusprobe:
|
||||||
scheduleDuration: 60
|
scheduleDuration: 60
|
||||||
maxKafkaFailureDuration: 5
|
|
||||||
maxBlobFailureCount: 5
|
maxBlobFailureCount: 5
|
||||||
maxFailurePercent: 50
|
maxFailurePercent: 50
|
||||||
de.twomartens.support.health.greeting: "Good morning"
|
de.twomartens.support.health.greeting: "Good morning"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import de.twomartens.support.model.dto.ErrorMessage
|
|||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.authorization.AuthorizationDeniedException
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
@@ -23,6 +24,12 @@ class ExceptionController : ResponseEntityExceptionHandler() {
|
|||||||
.body(ErrorMessage(e.message!!))
|
.body(ErrorMessage(e.message!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AuthorizationDeniedException::class)
|
||||||
|
fun handleDeniedException(e: AuthorizationDeniedException): ResponseEntity<ErrorMessage> {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ErrorMessage(e.message!!))
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(RuntimeException::class)
|
@ExceptionHandler(RuntimeException::class)
|
||||||
fun handleRuntimeException(e: RuntimeException): ResponseEntity<ErrorMessage> {
|
fun handleRuntimeException(e: RuntimeException): ResponseEntity<ErrorMessage> {
|
||||||
log.error("unexpected exception occurred", e)
|
log.error("unexpected exception occurred", e)
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package de.twomartens.support.security
|
|
||||||
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.keycloak.AuthorizationContext
|
|
||||||
import org.keycloak.adapters.authorization.PolicyEnforcer
|
|
||||||
import org.keycloak.adapters.authorization.spi.HttpRequest
|
|
||||||
import org.keycloak.adapters.authorization.spi.HttpResponse
|
|
||||||
import org.keycloak.authorization.client.ClientAuthorizationContext
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode
|
|
||||||
import org.keycloak.representations.idm.authorization.Permission
|
|
||||||
|
|
||||||
class SpringPolicyEnforcer(private val policyEnforcer: PolicyEnforcer,
|
|
||||||
private val policyEnforcerConfig: PolicyEnforcerConfig) {
|
|
||||||
|
|
||||||
fun enforce(request: HttpRequest, response: HttpResponse): AuthorizationContext {
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.uri)
|
|
||||||
}
|
|
||||||
val context = authorize(request, response)
|
|
||||||
if (log.isDebugEnabled) {
|
|
||||||
log.debug("Policy enforcement result for path [{}] is : {}", request.uri, if (context.isGranted) "GRANTED" else "DENIED")
|
|
||||||
log.debug("Returning authorization context with permissions:")
|
|
||||||
for (permission in context.permissions) {
|
|
||||||
log.debug(permission.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun authorize(request: HttpRequest, response: HttpResponse): AuthorizationContext {
|
|
||||||
val enforcementMode = policyEnforcerConfig.enforcementMode
|
|
||||||
return if (EnforcementMode.DISABLED == enforcementMode) {
|
|
||||||
createAuthorizedContext()
|
|
||||||
} else policyEnforcer.enforce(request, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createAuthorizedContext(): AuthorizationContext {
|
|
||||||
return object : ClientAuthorizationContext(policyEnforcer.authzClient) {
|
|
||||||
override fun hasPermission(resourceName: String, scopeName: String): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasResourcePermission(resourceName: String): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasScopePermission(scopeName: String): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPermissions(): List<Permission> {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isGranted(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = KotlinLogging.logger {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
package de.twomartens.support.security
|
|
||||||
|
|
||||||
import jakarta.servlet.*
|
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
|
||||||
import jakarta.servlet.http.HttpServletResponse
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.keycloak.AuthorizationContext
|
|
||||||
import org.keycloak.adapters.authorization.PolicyEnforcer
|
|
||||||
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpRequest
|
|
||||||
import org.keycloak.adapters.authorization.integration.elytron.ServletHttpResponse
|
|
||||||
import org.keycloak.adapters.authorization.spi.ConfigurationResolver
|
|
||||||
import org.keycloak.adapters.authorization.spi.HttpRequest
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver) : Filter {
|
|
||||||
@Throws(IOException::class, ServletException::class)
|
|
||||||
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) {
|
|
||||||
val request = servletRequest as HttpServletRequest
|
|
||||||
val response = servletResponse as HttpServletResponse?
|
|
||||||
val httpRequest = ServletHttpRequest(request) { extractBearerToken(request) }
|
|
||||||
val policyEnforcer = createPolicyEnforcer(httpRequest)
|
|
||||||
val authzContext = policyEnforcer.enforce(httpRequest, ServletHttpResponse(response))
|
|
||||||
request.setAttribute(AuthorizationContext::class.java.name, authzContext)
|
|
||||||
if (authzContext.isGranted) {
|
|
||||||
log.debug("Request authorized, continuing the filter chain")
|
|
||||||
filterChain.doFilter(servletRequest, servletResponse)
|
|
||||||
} else {
|
|
||||||
log.debug("Unauthorized request to path [{}], aborting the filter chain", request.requestURI)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractBearerToken(request: HttpServletRequest): String? {
|
|
||||||
val authorizationHeaderValues = request.getHeaders("Authorization")
|
|
||||||
while (authorizationHeaderValues.hasMoreElements()) {
|
|
||||||
val value = authorizationHeaderValues.nextElement()
|
|
||||||
val parts = value.trim()
|
|
||||||
.split("\\s+".toRegex())
|
|
||||||
.dropLastWhile { it.isEmpty() }
|
|
||||||
.toTypedArray()
|
|
||||||
if (parts.size != 2) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val bearer = parts[0]
|
|
||||||
if (bearer.equals("Bearer", ignoreCase = true)) {
|
|
||||||
return parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
|
|
||||||
return createPolicyEnforcer(configResolver.resolve(request))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPolicyEnforcer(enforcerConfig: PolicyEnforcerConfig): SpringPolicyEnforcer {
|
|
||||||
val authServerUrl = enforcerConfig.authServerUrl
|
|
||||||
return SpringPolicyEnforcer(PolicyEnforcer.builder()
|
|
||||||
.authServerUrl(authServerUrl)
|
|
||||||
.realm(enforcerConfig.realm)
|
|
||||||
.clientId(enforcerConfig.resource)
|
|
||||||
.credentials(enforcerConfig.credentials)
|
|
||||||
.bearerOnly(false)
|
|
||||||
.enforcerConfig(enforcerConfig).build(), enforcerConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = KotlinLogging.logger {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user