Moved security classes to Kotlin

This commit is contained in:
2023-07-29 16:37:46 +02:00
parent 713ccd9d46
commit 5f070ad649
11 changed files with 189 additions and 216 deletions
+5 -4
View File
@@ -1,9 +1,10 @@
plugins {
id("twomartens.spring-boot-cloud")
id("twomartens.spring-boot-cloud")
id("twomartens.kotlin")
}
dependencies {
implementation(libs.mapstruct.base)
implementation(libs.bundles.spring.boot.security)
annotationProcessor(libs.mapstruct.processor)
implementation(libs.mapstruct.base)
implementation(libs.bundles.spring.boot.security)
annotationProcessor(libs.mapstruct.processor)
}
@@ -1,23 +0,0 @@
package de.twomartens.wahlrecht;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableMongoAuditing
@EnableScheduling
@SpringBootApplication
public class MainApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(MainApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
@@ -31,7 +31,8 @@ import org.springframework.security.web.SecurityFilterChain;
public class WebSecurityConfiguration {
private static final Collection<String> PERMITTED_PATHS = List.of(
"/wahlrecht/healthCheck",
"/wahlrecht/v1/healthCheck",
"/actuator/**",
"/wahlrecht/v1/doc/**",
"/wahlrecht/v1/api-docs/**",
"/error");
@@ -1,86 +0,0 @@
package de.twomartens.wahlrecht.security;
import java.util.Collections;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
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;
/**
* Allows any request, even from anonymous, if enforcement mode is disabled. For remaining requests,
* it refers to the original PolicyEnforcer.
*/
@Slf4j
public class SpringPolicyEnforcer {
private final PolicyEnforcer policyEnforcer;
private final PolicyEnforcerConfig policyEnforcerConfig;
public SpringPolicyEnforcer(PolicyEnforcer policyEnforcer, PolicyEnforcerConfig policyEnforcerConfig) {
this.policyEnforcer = policyEnforcer;
this.policyEnforcerConfig = policyEnforcerConfig;
}
public AuthorizationContext enforce(HttpRequest request, HttpResponse response) {
if (log.isDebugEnabled()) {
log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.getURI());
}
AuthorizationContext context = authorize(request, response);
if (log.isDebugEnabled()) {
log.debug("Policy enforcement result for path [{}] is : {}", request.getURI(), context.isGranted() ? "GRANTED" : "DENIED");
log.debug("Returning authorization context with permissions:");
for (Permission permission : context.getPermissions()) {
log.debug(permission.toString());
}
}
return context;
}
private AuthorizationContext authorize(HttpRequest request, HttpResponse response) {
EnforcementMode enforcementMode = policyEnforcerConfig.getEnforcementMode();
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
return createAuthorizedContext();
}
return policyEnforcer.enforce(request, response);
}
private AuthorizationContext createAuthorizedContext() {
return new ClientAuthorizationContext(policyEnforcer.getAuthzClient()) {
@Override
public boolean hasPermission(String resourceName, String scopeName) {
return true;
}
@Override
public boolean hasResourcePermission(String resourceName) {
return true;
}
@Override
public boolean hasScopePermission(String scopeName) {
return true;
}
@Override
public List<Permission> getPermissions() {
return Collections.emptyList();
}
@Override
public boolean isGranted() {
return true;
}
};
}
}
@@ -1,102 +0,0 @@
package de.twomartens.wahlrecht.security;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.lang.NonNull;
/**
* A Jakarta Servlet {@link Filter} acting as a policy enforcer. This filter does not enforce access for anonymous subjects.</p>
*
* For authenticated subjects, this filter delegates the access decision to the {@link PolicyEnforcer} and decide if
* the request should continue.</p>
*
* If access is not granted, this filter aborts the request and relies on the {@link PolicyEnforcer} to properly
* respond to client.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Slf4j
public class SpringPolicyEnforcerFilter implements Filter {
private final Map<PolicyEnforcerConfig, SpringPolicyEnforcer> policyEnforcer;
private final ConfigurationResolver configResolver;
public SpringPolicyEnforcerFilter(ConfigurationResolver configResolver) {
this.configResolver = configResolver;
this.policyEnforcer = Collections.synchronizedMap(new HashMap<>());
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
ServletHttpRequest httpRequest = new ServletHttpRequest(request,
() -> extractBearerToken(request));
SpringPolicyEnforcer policyEnforcer = getOrCreatePolicyEnforcer(request, httpRequest);
AuthorizationContext authzContext = policyEnforcer.enforce(httpRequest, new ServletHttpResponse(response));
request.setAttribute(AuthorizationContext.class.getName(), 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.getRequestURI());
}
}
protected String extractBearerToken(@NonNull HttpServletRequest request) {
Enumeration<String> authorizationHeaderValues = request.getHeaders("Authorization");
while (authorizationHeaderValues.hasMoreElements()) {
String value = authorizationHeaderValues.nextElement();
String[] parts = value.trim().split("\\s+");
if (parts.length != 2) {
continue;
}
String bearer = parts[0];
if (bearer.equalsIgnoreCase("Bearer")) {
return parts[1];
}
}
return null;
}
private SpringPolicyEnforcer getOrCreatePolicyEnforcer(HttpServletRequest servletRequest, HttpRequest request) {
return policyEnforcer.computeIfAbsent(configResolver.resolve(request),
enforcerConfig -> createPolicyEnforcer(servletRequest, enforcerConfig));
}
protected SpringPolicyEnforcer createPolicyEnforcer(HttpServletRequest servletRequest, @NonNull PolicyEnforcerConfig enforcerConfig) {
String authServerUrl = enforcerConfig.getAuthServerUrl();
return new SpringPolicyEnforcer(PolicyEnforcer.builder()
.authServerUrl(authServerUrl)
.realm(enforcerConfig.getRealm())
.clientId(enforcerConfig.getResource())
.credentials(enforcerConfig.getCredentials())
.bearerOnly(false)
.enforcerConfig(enforcerConfig).build(), enforcerConfig);
}
}
@@ -0,0 +1,15 @@
package de.twomartens.wahlrecht
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.mongodb.config.EnableMongoAuditing
import org.springframework.scheduling.annotation.EnableScheduling
@EnableMongoAuditing
@EnableScheduling
@SpringBootApplication
open class MainApplication
fun main(args: Array<String>) {
runApplication<MainApplication>(*args)
}
@@ -0,0 +1,17 @@
package de.twomartens.wahlrecht.controller.v1
import io.swagger.v3.oas.annotations.Hidden
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping(value = ["/wahlrecht/v1"])
class HealthCheckController {
@Hidden
@GetMapping("/healthCheck")
fun healthCheck(message: String): String {
return message
}
}
@@ -0,0 +1,63 @@
package de.twomartens.wahlrecht.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
private val log = KotlinLogging.logger {}
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
}
}
}
}
@@ -0,0 +1,74 @@
package de.twomartens.wahlrecht.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
import java.util.*
private val log = KotlinLogging.logger {}
class SpringPolicyEnforcerFilter(private val configResolver: ConfigurationResolver?) : Filter {
private var policyEnforcer: MutableMap<PolicyEnforcerConfig, SpringPolicyEnforcer> = Collections.synchronizedMap(HashMap())
@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 = getOrCreatePolicyEnforcer(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 getOrCreatePolicyEnforcer(request: HttpRequest): SpringPolicyEnforcer {
return policyEnforcer.computeIfAbsent(configResolver!!.resolve(request)) { enforcerConfig: PolicyEnforcerConfig ->
createPolicyEnforcer(enforcerConfig)
}
}
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)
}
}