diff --git a/kofu/build.gradle.kts b/kofu/build.gradle.kts index 53230064d..04a42c70f 100644 --- a/kofu/build.gradle.kts +++ b/kofu/build.gradle.kts @@ -8,15 +8,20 @@ plugins { id("java-library") } +ext["spring-security.version"] = "5.2.0.BUILD-SNAPSHOT" +ext["spring-security-config.version"] = "5.2.0.BUILD-SNAPSHOT" + dependencies { api("org.springframework.boot:spring-boot") api("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation(project(":autoconfigure-adapter")) + implementation(project(":security-adapter")) implementation("org.jetbrains.kotlin:kotlin-reflect") compileOnly("org.springframework:spring-webmvc") compileOnly("org.springframework:spring-webflux") + compileOnly("org.springframework.boot:spring-boot-starter-security") compileOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo") compileOnly("org.springframework.data:spring-data-mongodb") compileOnly("org.mongodb:mongodb-driver-reactivestreams") @@ -37,6 +42,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-mustache") testImplementation("org.springframework.boot:spring-boot-starter-json") testImplementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive") + testImplementation("org.springframework.boot:spring-boot-starter-security") testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin") testRuntimeOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo") testImplementation("io.mockk:mockk:1.9") diff --git a/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/SecurityDsl.kt b/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/SecurityDsl.kt new file mode 100644 index 000000000..a4507bb3f --- /dev/null +++ b/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/SecurityDsl.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.fu.kofu.webflux + +import org.springframework.context.support.GenericApplicationContext +import org.springframework.fu.kofu.AbstractDsl +import org.springframework.fu.kofu.ConfigurationDsl +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.annotation.web.reactive.SecurityInitializer +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService +import org.springframework.security.core.userdetails.ReactiveUserDetailsService +import org.springframework.security.crypto.password.PasswordEncoder + +/** + * Kofu DSL for spring-security. + * + * Configure spring-security. + * + * Required dependencies can be retrieve using `org.springframework.boot:spring-boot-starter-security`. + * + * @author Jonas Bark + */ +class SecurityDsl(private val init: SecurityDsl.() -> Unit) : AbstractDsl() { + + var authenticationManager: ReactiveAuthenticationManager? = null + + var reactiveUserDetailsService: ReactiveUserDetailsService? = null + + var passwordEncoder: PasswordEncoder? = null + + var userDetailsPasswordService: ReactiveUserDetailsPasswordService? = null + + internal var httpSecurity: ServerHttpSecurity? = null + + override fun initialize(context: GenericApplicationContext) { + super.initialize(context) + init() + SecurityInitializer( + authenticationManager, + reactiveUserDetailsService, + passwordEncoder, + userDetailsPasswordService, + httpSecurity + ).initialize(context) + } + + + class HttpSecurityDsl( + private val init: ServerHttpSecurity.() -> Unit, + private val securityDsl: SecurityDsl + ) : AbstractDsl() { + + private val httpSecurity: ServerHttpSecurity = ServerHttpSecurity.http() + + override fun initialize(context: GenericApplicationContext) { + super.initialize(context) + httpSecurity.apply(init) + securityDsl.httpSecurity = httpSecurity + } + + } + + fun http(dsl: ServerHttpSecurity.() -> Unit = {}) { + HttpSecurityDsl(dsl, this).initialize(context) + } +} + +/** + * Configure spring-security. + * + * Require `org.springframework.boot:spring-boot-starter-security` dependency. + * + * @sample org.springframework.fu.kofu.samples.securityDsl + * @author Jonas Bark + */ +fun ConfigurationDsl.security(dsl: SecurityDsl.() -> Unit = {}) { + SecurityDsl(dsl).initialize(context) +} + + diff --git a/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/WebFluxSecurityDsl.kt b/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/WebFluxSecurityDsl.kt new file mode 100644 index 000000000..a4b1c6c9b --- /dev/null +++ b/kofu/src/main/kotlin/org/springframework/fu/kofu/webflux/WebFluxSecurityDsl.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.fu.kofu.webflux + +import org.springframework.context.support.GenericApplicationContext +import org.springframework.fu.kofu.AbstractDsl +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.annotation.web.reactive.SecurityInitializer +import org.springframework.security.config.annotation.web.reactive.WebFluxSecurityInitializer +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService +import org.springframework.security.core.userdetails.ReactiveUserDetailsService +import org.springframework.security.crypto.password.PasswordEncoder + +/** + * Kofu DSL for spring-security. + * + * Configure spring-security. + * + * Required dependencies can be retrieve using `org.springframework.boot:spring-boot-starter-security`. + * + * @author Jonas Bark + */ +class WebFluxSecurityDsl(private val init: WebFluxSecurityDsl.() -> Unit) : AbstractDsl() { + + override fun initialize(context: GenericApplicationContext) { + super.initialize(context) + init() + WebFluxSecurityInitializer().initialize(context) + } +} + +/** + * Configure spring-security. + * + * Require `org.springframework.boot:spring-boot-starter-security` dependency. + * + * @sample org.springframework.fu.kofu.samples.securityDsl + * @author Jonas Bark + */ +fun WebFluxServerDsl.webFluxSecurity(dsl: WebFluxSecurityDsl.() -> Unit = {}) { + WebFluxSecurityDsl(dsl).initialize(context) +} diff --git a/kofu/src/test/kotlin/org/springframework/fu/kofu/samples/security.kt b/kofu/src/test/kotlin/org/springframework/fu/kofu/samples/security.kt new file mode 100644 index 000000000..552a7f92f --- /dev/null +++ b/kofu/src/test/kotlin/org/springframework/fu/kofu/samples/security.kt @@ -0,0 +1,16 @@ +package org.springframework.fu.kofu.samples + +import org.springframework.boot.WebApplicationType +import org.springframework.fu.kofu.application +import org.springframework.fu.kofu.webflux.security +import org.springframework.fu.kofu.webflux.webFlux +import org.springframework.fu.kofu.webflux.webFluxSecurity + +fun securityDsl() { + application(WebApplicationType.REACTIVE) { + security() + webFlux { + webFluxSecurity() + } + } +} diff --git a/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SecurityDslTests.kt b/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SecurityDslTests.kt new file mode 100644 index 000000000..7e0f5c99e --- /dev/null +++ b/kofu/src/test/kotlin/org/springframework/fu/kofu/webflux/SecurityDslTests.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.fu.kofu.webflux + +import org.junit.jupiter.api.Test +import org.springframework.boot.WebApplicationType +import org.springframework.fu.kofu.application +import org.springframework.fu.kofu.localServerPort +import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.test.web.reactive.server.WebTestClient +import java.nio.charset.Charset +import java.util.* + + +/** + * @author Jonas Bark + */ +class SecurityDslTests { + + @Test + fun `Check spring-security configuration DSL`() { + + val username = "user" + val password = "password" + val repoAuthenticationManager = + UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService(username, password)) + + val app = application(WebApplicationType.REACTIVE) { + security { + authenticationManager = repoAuthenticationManager + http { + authenticationManager(repoAuthenticationManager) + headers() + logout() + } + } + webFlux { + port = 0 + webFluxSecurity() + router { + GET("/view") { ok().build() } + } + } + } + with(app.run()) { + val client = WebTestClient.bindToServer().baseUrl("http://127.0.0.1:$localServerPort").build() + client.get().uri("/view").exchange() + .expectStatus().isUnauthorized + + val basicAuth = + Base64.getEncoder().encode("$username:$password".toByteArray())?.toString(Charset.defaultCharset()) + client.get().uri("/view").header("Authorization", "Basic $basicAuth").exchange() + .expectStatus().is2xxSuccessful + + close() + } + } + + private fun userDetailsService(username: String, password: String): MapReactiveUserDetailsService { + @Suppress("DEPRECATION") + val user = User.withDefaultPasswordEncoder() + .username(username) + .password(password) + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + +} diff --git a/security-adapter/README.adoc b/security-adapter/README.adoc new file mode 100644 index 000000000..66457f983 --- /dev/null +++ b/security-adapter/README.adoc @@ -0,0 +1,14 @@ += Spring Boot auto-configuration adapter + +This Java library adapts JavaConfig based `spring-boot-autoconfigure` to functional +configuration based on function bean registration which is known to be faster and +consumes less memory. + +It provides a collection of `ApplicationContextInitializer` that +leverage Spring Boot auto-configurations to register the same beans but in a functional +way. For now, it mostly targets the Reactive stack and conditions are not managed yet. + +Ideally in the future, such library should be generated during Spring Boot build based +on `spring-boot-autoconfigure` classes. + +The dependency to use is `org.springframework.fu:spring-fu-security-adapter`. diff --git a/security-adapter/build.gradle.kts b/security-adapter/build.gradle.kts new file mode 100644 index 000000000..b6b28c611 --- /dev/null +++ b/security-adapter/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("io.spring.dependency-management") + id("java-library") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +ext["spring-security.version"] = "5.2.0.BUILD-SNAPSHOT" +ext["spring-security-config.version"] = "5.2.0.BUILD-SNAPSHOT" + +dependencies { + api("org.springframework.boot:spring-boot") + api("org.springframework.boot:spring-boot-autoconfigure") + + compileOnly("org.springframework:spring-webflux") + compileOnly("org.springframework.boot:spring-boot-starter-security") +} + +repositories { + maven("https://repo.spring.io/milestone") +} + +publishing { + publications { + create(project.name, MavenPublication::class.java) { + from(components["java"]) + artifactId = "spring-fu-security-adapter" + val sourcesJar by tasks.creating(Jar::class) { + classifier = "sources" + from(sourceSets["main"].allSource) + } + artifact(sourcesJar) + } + } +} diff --git a/security-adapter/src/main/java/org/springframework/security/config/annotation/web/reactive/SecurityInitializer.java b/security-adapter/src/main/java/org/springframework/security/config/annotation/web/reactive/SecurityInitializer.java new file mode 100644 index 000000000..95e98676e --- /dev/null +++ b/security-adapter/src/main/java/org/springframework/security/config/annotation/web/reactive/SecurityInitializer.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.reactive; + +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver; +import org.springframework.security.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver; + +/** + * {@link ApplicationContextInitializer} adapter for {@link org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration}. + */ +public class SecurityInitializer implements ApplicationContextInitializer { + + private ReactiveAuthenticationManager authenticationManager; + private ReactiveUserDetailsService reactiveUserDetailsService; + private PasswordEncoder passwordEncoder; + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + private ServerHttpSecurity httpSecurity; + + /** + * @see ServerHttpSecurityConfiguration + * @param authenticationManager {@link ServerHttpSecurityConfiguration} + * @param reactiveUserDetailsService {@link ServerHttpSecurityConfiguration} + * @param passwordEncoder {@link ServerHttpSecurityConfiguration} + * @param userDetailsPasswordService {@link ServerHttpSecurityConfiguration} + * @param httpSecurity {@link ServerHttpSecurityConfiguration} + */ + public SecurityInitializer(ReactiveAuthenticationManager authenticationManager, ReactiveUserDetailsService reactiveUserDetailsService, PasswordEncoder passwordEncoder, ReactiveUserDetailsPasswordService userDetailsPasswordService, ServerHttpSecurity httpSecurity) { + this.authenticationManager = authenticationManager; + this.reactiveUserDetailsService = reactiveUserDetailsService; + this.passwordEncoder = passwordEncoder; + this.userDetailsPasswordService = userDetailsPasswordService; + this.httpSecurity = httpSecurity; + } + + @Override + public void initialize(GenericApplicationContext context) { + ServerHttpSecurityConfiguration configuration = new ServerHttpSecurityConfiguration(); + + if (authenticationManager != null) { + configuration.setAuthenticationManager(authenticationManager); + } + if (reactiveUserDetailsService != null) { + configuration.setReactiveUserDetailsService(reactiveUserDetailsService); + } + if (passwordEncoder != null) { + configuration.setPasswordEncoder(passwordEncoder); + } + if (userDetailsPasswordService != null) { + configuration.setUserDetailsPasswordService(userDetailsPasswordService); + } + + context.registerBean(AuthenticationPrincipalArgumentResolver.class, configuration::authenticationPrincipalArgumentResolver); + context.registerBean(CurrentSecurityContextArgumentResolver.class, configuration::reactiveCurrentSecurityContextArgumentResolver); + context.registerBean( + "org.springframework.security.config.annotation.web.reactive.HttpSecurityConfiguration.httpSecurity", + ServerHttpSecurity.class, + () -> httpSecurity != null ? httpSecurity : configuration.httpSecurity(), + (BeanDefinitionCustomizer) bd -> { + bd.setScope("prototype"); + bd.setAutowireCandidate(true); + } + ); + } +} diff --git a/security-adapter/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityInitializer.java b/security-adapter/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityInitializer.java new file mode 100644 index 000000000..ebd34cda8 --- /dev/null +++ b/security-adapter/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityInitializer.java @@ -0,0 +1,34 @@ +package org.springframework.security.config.annotation.web.reactive; + +import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; +import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.web.reactive.result.view.AbstractView; + +/** + * {@link ApplicationContextInitializer} adapter for {@link WebFluxSecurityConfiguration}. + */ +public class WebFluxSecurityInitializer implements ApplicationContextInitializer { + + + public WebFluxSecurityInitializer() { + } + + @Override + public void initialize(GenericApplicationContext context) { + WebFluxSecurityConfiguration configuration = new WebFluxSecurityConfiguration(); + configuration.context = context; + context.registerBean( + "org.springframework.security.config.annotation.web.reactive.WebFluxSecurityConfiguration.WebFilterChainFilter", + WebFilterChainProxy.class, + configuration::springSecurityWebFilterChainFilter + ); + context.registerBean( + AbstractView.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, + CsrfRequestDataValueProcessor.class, + configuration::requestDataValueProcessor + ); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a8815a48e..37302d289 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ rootProject.name = "spring-fu-build" include( "autoconfigure-adapter", + "security-adapter", "kofu" )