diff --git a/reactive/webflux/method/build.gradle b/reactive/webflux/method/build.gradle new file mode 100644 index 0000000..db61d30 --- /dev/null +++ b/reactive/webflux/method/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '2.3.1.RELEASE' + id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id "nebula.integtest" version "7.0.9" + id 'java' +} + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} diff --git a/reactive/webflux/method/src/integTest/java/example/HelloMethodApplicationITests.java b/reactive/webflux/method/src/integTest/java/example/HelloMethodApplicationITests.java new file mode 100644 index 0000000..1a1b675 --- /dev/null +++ b/reactive/webflux/method/src/integTest/java/example/HelloMethodApplicationITests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 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 + * + * https://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 example; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests. + * + * @author Rob Winch + * @since 5.0 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class HelloMethodApplicationITests { + + @Autowired + WebTestClient rest; + + // --- /message --- + + @Test + void messageWhenNotAuthenticated() { + // @formatter:off + this.rest.get() + .uri("/message") + .exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + void messageWhenUserThenOk() { + // @formatter:off + this.rest.get() + .uri("/message") + .headers(userCredentials()) + .exchange() + .expectStatus().isOk(); + // @formatter:on + } + + // --- /secret --- + + @Test + void secretWhenNotAuthenticated() { + // @formatter:off + this.rest.get() + .uri("/secret") + .exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + void secretWhenUserThenForbidden() { + // @formatter:off + this.rest.get() + .uri("/secret") + .headers(userCredentials()) + .exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + @Test + void secretWhenAdminThenOk() { + // @formatter:off + this.rest.get() + .uri("/secret") + .headers(adminCredentials()) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello Admin!"); + // @formatter:on + } + + private Consumer userCredentials() { + return (httpHeaders) -> httpHeaders.setBasicAuth("user", "password"); + } + + private Consumer adminCredentials() { + return (httpHeaders) -> httpHeaders.setBasicAuth("admin", "password"); + } + +} diff --git a/reactive/webflux/method/src/main/java/example/HelloMethodApplication.java b/reactive/webflux/method/src/main/java/example/HelloMethodApplication.java new file mode 100644 index 0000000..a8e0217 --- /dev/null +++ b/reactive/webflux/method/src/main/java/example/HelloMethodApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 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 + * + * https://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 example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Simple application that uses method security. + * + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class HelloMethodApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloMethodApplication.class, args); + } + +} diff --git a/reactive/webflux/method/src/main/java/example/MessageController.java b/reactive/webflux/method/src/main/java/example/MessageController.java new file mode 100644 index 0000000..2a04f4b --- /dev/null +++ b/reactive/webflux/method/src/main/java/example/MessageController.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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 + * + * https://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 example; + +import reactor.core.publisher.Mono; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller for the messages. + * + * @author Rob Winch + * @since 5.0 + */ +@RestController +public class MessageController { + + private final MessageService messages; + + public MessageController(MessageService messages) { + this.messages = messages; + } + + @GetMapping("/message") + public Mono message() { + return this.messages.findMessage(); + } + + @GetMapping("/secret") + public Mono secretMessage() { + return this.messages.findSecretMessage(); + } + +} diff --git a/reactive/webflux/method/src/main/java/example/MessageService.java b/reactive/webflux/method/src/main/java/example/MessageService.java new file mode 100644 index 0000000..8a40d85 --- /dev/null +++ b/reactive/webflux/method/src/main/java/example/MessageService.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 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 + * + * https://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 example; + +import reactor.core.publisher.Mono; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Message service that has method security on it. + * + * @author Rob Winch + * @since 5.0 + */ +@Component +public class MessageService { + + /** + * Gets a message if authenticated. + * @return the message + */ + @PreAuthorize("authenticated") + public Mono findMessage() { + return Mono.just("Hello User!"); + } + + /** + * Gets a message if admin. + * @return the message + */ + @PreAuthorize("hasRole('ADMIN')") + public Mono findSecretMessage() { + return Mono.just("Hello Admin!"); + } + +} diff --git a/reactive/webflux/method/src/main/java/example/SecurityConfiguration.java b/reactive/webflux/method/src/main/java/example/SecurityConfiguration.java new file mode 100644 index 0000000..2ba25af --- /dev/null +++ b/reactive/webflux/method/src/main/java/example/SecurityConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 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 + * + * https://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 example; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +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.security.core.userdetails.UserDetails; +import org.springframework.security.web.server.SecurityWebFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * Minimal method security configuration. + * + * @author Rob Winch + * @since 5.0 + */ +@Configuration +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity +public class SecurityConfiguration { + + @Bean + SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + http + // Demonstrate that method security works + // Best practice to use both for defense in depth + .authorizeExchange((exchanges) -> exchanges + .anyExchange().permitAll() + ) + .httpBasic(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + MapReactiveUserDetailsService userDetailsService() { + // @formatter:off + UserDetails user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + UserDetails admin = User.withDefaultPasswordEncoder() + .username("admin") + .password("password") + .roles("ADMIN", "USER") + .build(); + // @formatter:on + return new MapReactiveUserDetailsService(user, admin); + } + +} diff --git a/reactive/webflux/method/src/test/java/example/HelloMethodApplicationTests.java b/reactive/webflux/method/src/test/java/example/HelloMethodApplicationTests.java new file mode 100644 index 0000000..5d4a135 --- /dev/null +++ b/reactive/webflux/method/src/test/java/example/HelloMethodApplicationTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 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 + * + * https://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 example; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootTest +@AutoConfigureWebTestClient +public class HelloMethodApplicationTests { + + @Autowired + WebTestClient rest; + + // --- /message --- + + @Test + void messageWhenNotAuthenticatedThenUnAuthorized() { + // @formatter:off + this.rest.get() + .uri("/message") + .exchange(). + expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + @WithMockUser + void messageWhenAuthenticatedThenOk() { + // @formatter:off + this.rest.get() + .uri("/message") + .exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + // --- /secret --- + + @Test + void secretWhenNotAuthenticatedThenUnAuthorized() { + // @formatter:off + this.rest.get() + .uri("/secret") + .exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + @WithMockUser + void secretWhenNotAuthorizedThenForbidden() { + // @formatter:off + this.rest.get() + .uri("/secret") + .exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + @Test + @WithMockUser(roles = "ADMIN") + void secretWhenAuthorizedThenOk() { + // @formatter:off + this.rest.get() + .uri("/secret") + .exchange() + .expectStatus().isOk(); + // @formatter:on + } + +} diff --git a/reactive/webflux/method/src/test/java/example/MessageServiceTests.java b/reactive/webflux/method/src/test/java/example/MessageServiceTests.java new file mode 100644 index 0000000..f4e9a2d --- /dev/null +++ b/reactive/webflux/method/src/test/java/example/MessageServiceTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 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 + * + * https://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 example; + +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithMockUser; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootTest +public class MessageServiceTests { + + @Autowired + MessageService messages; + + // -- findMessage --- + + @Test + void findMessageWhenNotAuthenticatedThenDenied() { + // @formatter:off + StepVerifier.create(this.messages.findMessage()) + .expectError(AccessDeniedException.class) + .verify(); + // @formatter:on + } + + @Test + @WithMockUser + void findMessageWhenUserThenDenied() { + // @formatter:off + StepVerifier.create(this.messages.findMessage()) + .expectNext("Hello User!") + .verifyComplete(); + // @formatter:on + } + + // -- findSecretMessage --- + + @Test + void findSecretMessageWhenNotAuthenticatedThenDenied() { + // @formatter:off + StepVerifier.create(this.messages.findSecretMessage()) + .expectError(AccessDeniedException.class) + .verify(); + // @formatter:on + } + + @Test + @WithMockUser + void findSecretMessageWhenNotAuthorizedThenDenied() { + // @formatter:off + StepVerifier.create(this.messages.findSecretMessage()) + .expectError(AccessDeniedException.class) + .verify(); + // @formatter:on + } + + @Test + @WithMockUser(roles = "ADMIN") + void findSecretMessageWhenAuthorizedThenSuccess() { + // @formatter:off + StepVerifier.create(this.messages.findSecretMessage()) + .expectNext("Hello Admin!") + .verifyComplete(); + // @formatter:on + } + +} diff --git a/settings.gradle b/settings.gradle index 9192319..259e8f8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,8 +15,10 @@ pluginManagement { include ':reactive:rsocket:hello-security' include ':reactive:webflux:hello' +include ':reactive:webflux:Initializationmethod' include ':reactive:webflux:hello-security' include ':reactive:webflux:hello-security-explicit' +include ':reactive:webflux:method' include ':servlet:spring-boot:java:hello' include ':servlet:spring-boot:java:hello-security' include ':servlet:spring-boot:java:hello-security-explicit'