diff --git a/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle b/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle new file mode 100644 index 0000000000..53102778f1 --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle @@ -0,0 +1,17 @@ +apply plugin: 'io.spring.convention.spring-sample' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-webflux') + compile 'com.fasterxml.jackson.core:jackson-databind' + compile 'io.netty:netty-buffer' + compile 'io.projectreactor.ipc:reactor-netty' + compile 'org.springframework:spring-context' + compile 'org.springframework:spring-webflux' + + testCompile project(':spring-security-test') + testCompile 'io.projectreactor:reactor-test' + testCompile 'org.skyscreamer:jsonassert' + testCompile 'org.springframework:spring-test' +} diff --git a/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java b/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java new file mode 100644 index 0000000000..e3618581fd --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2017 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 sample; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.web.server.header.ContentTypeOptionsHttpHeadersWriter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.ExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; + +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.Base64; +import java.util.Map; +import java.util.function.Consumer; + +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) +@TestPropertySource(properties = "server.port=0") +public class HelloWebfluxMethodApplicationITests { + @Value("#{@nettyContext.address().getPort()}") + int port; + + WebTestClient rest; + + @Before + public void setup() { + this.rest = WebTestClient.bindToServer() + .filter(basicAuthentication()) + .responseTimeout(Duration.ofDays(1)) + .baseUrl("http://localhost:" + this.port) + .build(); + } + + @Test + public void messageWhenNotAuthenticated() throws Exception { + this.rest + .get() + .uri("/message") + .exchange() + .expectStatus().isUnauthorized(); + } + + @Test + public void messageWhenUserThenForbidden() throws Exception { + this.rest + .get() + .uri("/message") + .attributes(robsCredentials()) + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) + .expectBody().isEmpty(); + } + + @Test + public void messageWhenAdminThenOk() throws Exception { + this.rest + .get() + .uri("/message") + .attributes(adminCredentials()) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World!"); + } + + private Consumer> robsCredentials() { + return basicAuthenticationCredentials("rob","rob"); + } + + private Consumer> adminCredentials() { + return basicAuthenticationCredentials("admin","admin"); + } + + private String base64Encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset())); + } +} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java b/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java new file mode 100644 index 0000000000..7671cce002 --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2017 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 sample; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.*; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.config.EnableWebFlux; +import reactor.ipc.netty.NettyContext; +import reactor.ipc.netty.http.server.HttpServer; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Configuration +@EnableWebFlux +@ComponentScan +public class HelloWebfluxMethodApplication { + @Value("${server.port:8080}") + private int port = 8080; + + public static void main(String[] args) throws Exception { + try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + HelloWebfluxMethodApplication.class)) { + context.getBean(NettyContext.class).onClose().block(); + } + } + + @Profile("default") + @Bean + public NettyContext nettyContext(ApplicationContext context) { + HttpHandler handler = DispatcherHandler.toHttpHandler(context); + ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); + HttpServer httpServer = HttpServer.create("localhost", port); + return httpServer.newHandler(adapter).block(); + } +} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java b/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java new file mode 100644 index 0000000000..ff6f6ade5a --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java @@ -0,0 +1,35 @@ +/* + * + * * Copyright 2002-2017 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 sample; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Component +public class HelloWorldMessageService { + @PreAuthorize("hasRole('ADMIN')") + public Mono findMessage() { + return Mono.just("Hello World!"); + } +} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/MessageController.java b/samples/javaconfig/hellowebflux-method/src/main/java/sample/MessageController.java new file mode 100644 index 0000000000..6e8d96c539 --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/src/main/java/sample/MessageController.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2017 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 sample; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RestController +public class MessageController { + private final HelloWorldMessageService messages; + + public MessageController(HelloWorldMessageService messages) { + this.messages = messages; + } + + @GetMapping("/message") + public Mono message() { + return this.messages.findMessage(); + } +} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/SecurityConfig.java b/samples/javaconfig/hellowebflux-method/src/main/java/sample/SecurityConfig.java new file mode 100644 index 0000000000..8cbcbda707 --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/src/main/java/sample/SecurityConfig.java @@ -0,0 +1,59 @@ +/* + * + * * Copyright 2002-2017 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 sample; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.core.userdetails.MapUserDetailsRepository; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authorization.AuthorizationContext; +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.0 + */ +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity +public class SecurityConfig { + + @Bean + SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception { + return http + // we rely on method security + .authorizeExchange() + .anyExchange().permitAll() + .and() + .build(); + } + + @Bean + public MapUserDetailsRepository userDetailsRepository() { + UserDetails rob = User.withUsername("rob").password("rob").roles("USER").build(); + UserDetails admin = User.withUsername("admin").password("admin").roles("USER","ADMIN").build(); + return new MapUserDetailsRepository(rob, admin); + } + +} diff --git a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java b/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java new file mode 100644 index 0000000000..cee3167684 --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java @@ -0,0 +1,104 @@ +/* + * + * * Copyright 2002-2017 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 sample; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +import java.nio.charset.Charset; +import java.util.Base64; +import java.util.Map; +import java.util.function.Consumer; + +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) +@ActiveProfiles("test") +public class HelloWebfluxMethodApplicationTests { + @Autowired + ApplicationContext context; + + WebTestClient rest; + + @Before + public void setup() { + this.rest = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .filter(basicAuthentication()) + .build(); + } + + @Test + public void messageWhenNotAuthenticated() throws Exception { + this.rest + .get() + .uri("/message") + .exchange() + .expectStatus().isUnauthorized(); + } + + @Test + public void messageWhenUserThenForbidden() throws Exception { + this.rest + .get() + .uri("/message") + .attributes(robsCredentials()) + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) + .expectBody().isEmpty(); + } + + @Test + public void messageWhenAdminThenOk() throws Exception { + this.rest + .get() + .uri("/message") + .attributes(adminCredentials()) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World!"); + } + + private Consumer> robsCredentials() { + return basicAuthenticationCredentials("rob","rob"); + } + + + private Consumer> adminCredentials() { + return basicAuthenticationCredentials("admin","admin"); + } + + private String base64Encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset())); + } +} diff --git a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java b/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java new file mode 100644 index 0000000000..2718a7e93e --- /dev/null +++ b/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java @@ -0,0 +1,64 @@ +/* + * + * * Copyright 2002-2017 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 sample; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import reactor.test.StepVerifier; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) +@ActiveProfiles("test") +public class HelloWorldMessageServiceTests { + @Autowired + HelloWorldMessageService messages; + + @Test + public void messagesWhenNotAuthenticatedThenDenied() { + StepVerifier.create(this.messages.findMessage()) + .expectError(AccessDeniedException.class) + .verify(); + } + + @Test + @WithMockUser + public void messagesWhenUserThenDenied() { + StepVerifier.create(this.messages.findMessage()) + .expectError(AccessDeniedException.class) + .verify(); + } + + @Test + @WithMockUser(roles = "ADMIN") + public void messagesWhenAdminThenOk() { + StepVerifier.create(this.messages.findMessage()) + .expectNext("Hello World!") + .verifyComplete(); + } +}