From 5f070ad6495e424216192cbe16d69c090d3833d8 Mon Sep 17 00:00:00 2001
From: Jim Martens
Date: Sat, 29 Jul 2023 16:37:14 +0200
Subject: [PATCH] Moved security classes to Kotlin
---
.../main/kotlin/twomartens.kotlin.gradle.kts | 11 ++
gradle/libs.versions.toml | 2 +
module-server/server.gradle.kts | 9 +-
.../twomartens/wahlrecht/MainApplication.java | 23 ----
.../WebSecurityConfiguration.java | 3 +-
.../security/SpringPolicyEnforcer.java | 86 ---------------
.../security/SpringPolicyEnforcerFilter.java | 102 ------------------
.../twomartens/wahlrecht/MainApplication.kt | 15 +++
.../controller/v1/HealthCheckController.kt | 17 +++
.../security/SpringPolicyEnforcer.kt | 63 +++++++++++
.../security/SpringPolicyEnforcerFilter.kt | 74 +++++++++++++
11 files changed, 189 insertions(+), 216 deletions(-)
create mode 100644 buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts
delete mode 100644 module-server/src/main/java/de/twomartens/wahlrecht/MainApplication.java
delete mode 100644 module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java
delete mode 100644 module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java
create mode 100644 module-server/src/main/kotlin/de/twomartens/wahlrecht/MainApplication.kt
create mode 100644 module-server/src/main/kotlin/de/twomartens/wahlrecht/controller/v1/HealthCheckController.kt
create mode 100644 module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.kt
create mode 100644 module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.kt
diff --git a/buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts b/buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts
new file mode 100644
index 0000000..c5952e4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/twomartens.kotlin.gradle.kts
@@ -0,0 +1,11 @@
+import org.gradle.accessors.dm.LibrariesForLibs
+
+plugins {
+ kotlin("jvm")
+}
+
+val libs = the()
+
+dependencies {
+ implementation(libs.kotlin.logging)
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9c6b1ed..4fc28db 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,6 +14,7 @@ junit = "5.9.2"
assertj = "3.24.2"
mockito = "5.3.0"
keycloak = "22.0.0"
+kotlin-logging = "2.0.11"
plugin-nebula-release = "17.2.2"
plugin-lombok = "8.0.1"
plugin-gradle-versions = "0.46.0"
@@ -68,6 +69,7 @@ assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-inline = "org.mockito:mockito-inline:5.2.0"
mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
+kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" }
plugin-nebula-release = { module = "com.netflix.nebula:nebula-release-plugin", version.ref = "plugin-nebula-release" }
plugin-springboot = { module = "org.springframework.boot:spring-boot-gradle-plugin", version.ref = "spring-boot" }
plugin-lombok = { module = "io.freefair.gradle:lombok-plugin", version.ref = "plugin-lombok" }
diff --git a/module-server/server.gradle.kts b/module-server/server.gradle.kts
index 500f575..be571e6 100644
--- a/module-server/server.gradle.kts
+++ b/module-server/server.gradle.kts
@@ -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)
}
\ No newline at end of file
diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/MainApplication.java b/module-server/src/main/java/de/twomartens/wahlrecht/MainApplication.java
deleted file mode 100644
index 2dcf179..0000000
--- a/module-server/src/main/java/de/twomartens/wahlrecht/MainApplication.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java b/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java
index d82f41d..18836a9 100644
--- a/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java
+++ b/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java
@@ -31,7 +31,8 @@ import org.springframework.security.web.SecurityFilterChain;
public class WebSecurityConfiguration {
private static final Collection PERMITTED_PATHS = List.of(
- "/wahlrecht/healthCheck",
+ "/wahlrecht/v1/healthCheck",
+ "/actuator/**",
"/wahlrecht/v1/doc/**",
"/wahlrecht/v1/api-docs/**",
"/error");
diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java b/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java
deleted file mode 100644
index 77b5172..0000000
--- a/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java
+++ /dev/null
@@ -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 getPermissions() {
- return Collections.emptyList();
- }
-
- @Override
- public boolean isGranted() {
- return true;
- }
- };
- }
-}
diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java b/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java
deleted file mode 100644
index bbc84c7..0000000
--- a/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java
+++ /dev/null
@@ -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.
- *
- * For authenticated subjects, this filter delegates the access decision to the {@link PolicyEnforcer} and decide if
- * the request should continue.
- *
- * If access is not granted, this filter aborts the request and relies on the {@link PolicyEnforcer} to properly
- * respond to client.
- *
- * @author Pedro Igor
- */
-@Slf4j
-public class SpringPolicyEnforcerFilter implements Filter {
- private final Map 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 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);
- }
-}
diff --git a/module-server/src/main/kotlin/de/twomartens/wahlrecht/MainApplication.kt b/module-server/src/main/kotlin/de/twomartens/wahlrecht/MainApplication.kt
new file mode 100644
index 0000000..c65ae75
--- /dev/null
+++ b/module-server/src/main/kotlin/de/twomartens/wahlrecht/MainApplication.kt
@@ -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) {
+ runApplication(*args)
+}
\ No newline at end of file
diff --git a/module-server/src/main/kotlin/de/twomartens/wahlrecht/controller/v1/HealthCheckController.kt b/module-server/src/main/kotlin/de/twomartens/wahlrecht/controller/v1/HealthCheckController.kt
new file mode 100644
index 0000000..5918252
--- /dev/null
+++ b/module-server/src/main/kotlin/de/twomartens/wahlrecht/controller/v1/HealthCheckController.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.kt b/module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.kt
new file mode 100644
index 0000000..7b187f3
--- /dev/null
+++ b/module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.kt
@@ -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 {
+ return emptyList()
+ }
+
+ override fun isGranted(): Boolean {
+ return true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.kt b/module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.kt
new file mode 100644
index 0000000..890ccfd
--- /dev/null
+++ b/module-server/src/main/kotlin/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.kt
@@ -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 = 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)
+ }
+}
\ No newline at end of file