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