From f01989ff4953ba6ad7107ace8e6e2e7cf1019cb6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 16 May 2017 15:38:07 -0500 Subject: [PATCH] Add hellowebfluxfn Fixes gh-4338 --- ...y-samples-javaconfig-hellowebfluxfn.gradle | 16 ++ .../HelloWebfluxFnApplicationTests.java | 199 ++++++++++++++++++ .../java/sample/UserRepositoryTests.java | 51 +++++ .../sample/HelloWebfluxFnApplication.java | 115 ++++++++++ .../main/java/sample/MapUserRepository.java | 58 +++++ .../src/main/java/sample/User.java | 84 ++++++++ .../src/main/java/sample/UserController.java | 65 ++++++ .../src/main/java/sample/UserRepository.java | 33 +++ .../UserRepositoryUserDetailsRepository.java | 87 ++++++++ 9 files changed, 708 insertions(+) create mode 100644 samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle create mode 100644 samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java create mode 100644 samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/UserRepositoryTests.java create mode 100644 samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java create mode 100644 samples/javaconfig/hellowebfluxfn/src/main/java/sample/MapUserRepository.java create mode 100644 samples/javaconfig/hellowebfluxfn/src/main/java/sample/User.java create mode 100644 samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserController.java create mode 100644 samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepository.java create mode 100644 samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepositoryUserDetailsRepository.java diff --git a/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle b/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle new file mode 100644 index 0000000000..2b6dd88356 --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle @@ -0,0 +1,16 @@ +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 'io.projectreactor.addons:reactor-test' + testCompile 'org.skyscreamer:jsonassert' + testCompile 'org.springframework:spring-test' +} diff --git a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java b/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java new file mode 100644 index 0000000000..6052487023 --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java @@ -0,0 +1,199 @@ +/* + * 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 static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = HelloWebfluxFnApplication.class) +@TestPropertySource(properties = "server.port=0") +public class HelloWebfluxFnApplicationTests { + @Value("#{@nettyContext.address().getPort()}") + int port; + + WebTestClient rest; + + @Before + public void setup() { + this.rest = WebTestClient.bindToServer() + .responseTimeout(Duration.ofDays(1)) + .baseUrl("http://localhost:" + this.port) + .build(); + } + + @Test + public void basicRequired() throws Exception { + this.rest + .get() + .uri("/users") + .exchange() + .expectStatus().isUnauthorized(); + } + + @Test + public void basicWorks() throws Exception { + this.rest + .filter(robsCredentials()) + .get() + .uri("/users") + .exchange() + .expectStatus().isOk() + .expectBody().json("[{\"id\":null,\"username\":\"rob\",\"password\":\"rob\",\"firstname\":\"Rob\",\"lastname\":\"Winch\"},{\"id\":null,\"username\":\"admin\",\"password\":\"admin\",\"firstname\":\"Admin\",\"lastname\":\"User\"}]"); + } + + @Test + public void basicWhenPasswordInvalid401() throws Exception { + this.rest + .filter(invalidPassword()) + .get() + .uri("/users") + .exchange() + .expectStatus().isUnauthorized() + .expectBody().isEmpty(); + } + + @Test + public void authorizationAdmin403() throws Exception { + this.rest + .filter(robsCredentials()) + .get() + .uri("/admin") + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) + .expectBody().isEmpty(); + } + + @Test + public void authorizationAdmin200() throws Exception { + this.rest + .filter(adminCredentials()) + .get() + .uri("/admin") + .exchange() + .expectStatus().isOk(); + } + + @Test + public void basicMissingUser401() throws Exception { + this.rest + .filter(basicAuthentication("missing-user", "password")) + .get() + .uri("/admin") + .exchange() + .expectStatus().isUnauthorized(); + } + + @Test + public void basicInvalidPassword401() throws Exception { + this.rest + .filter(invalidPassword()) + .get() + .uri("/admin") + .exchange() + .expectStatus().isUnauthorized(); + } + + @Test + public void basicInvalidParts401() throws Exception { + this.rest + .get() + .uri("/admin") + .header("Authorization", "Basic " + base64Encode("no colon")) + .exchange() + .expectStatus().isUnauthorized(); + } + + @Test + public void sessionWorks() throws Exception { + ExchangeResult result = this.rest + .filter(robsCredentials()) + .get() + .uri("/users") + .exchange() + .returnResult(String.class); + + String session = result.getResponseHeaders().getFirst("Set-Cookie"); + + this.rest + .get() + .uri("/users") + .header("Cookie", session) + .exchange() + .expectStatus().isOk(); + } + + @Test + public void principal() throws Exception { + this.rest + .filter(robsCredentials()) + .get() + .uri("/principal") + .exchange() + .expectStatus().isOk() + .expectBody().json("{\"username\" : \"rob\"}"); + } + + @Test + public void headers() throws Exception { + this.rest + .filter(robsCredentials()) + .get() + .uri("/principal") + .exchange() + .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0") + .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache") + .expectHeader().valueEquals(ContentTypeOptionsHttpHeadersWriter.X_CONTENT_OPTIONS, ContentTypeOptionsHttpHeadersWriter.NOSNIFF); + } + + private ExchangeFilterFunction robsCredentials() { + return basicAuthentication("rob","rob"); + } + + private ExchangeFilterFunction invalidPassword() { + return basicAuthentication("rob","INVALID"); + } + + private ExchangeFilterFunction adminCredentials() { + return basicAuthentication("admin","admin"); + } + + private String base64Encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset())); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/UserRepositoryTests.java b/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/UserRepositoryTests.java new file mode 100644 index 0000000000..1ba839f082 --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/UserRepositoryTests.java @@ -0,0 +1,51 @@ +/* + * 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.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Rob Winch + * @since 5.0 + */ +@SuppressWarnings("unused") +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = HelloWebfluxFnApplication.class) +@TestPropertySource(properties = "server.port=0") +public class UserRepositoryTests { + + @Autowired UserRepository repository; + + String robUsername = "rob"; + + @Test + public void findByUsernameWhenUsernameMatchesThenFound() { + assertThat(repository.findByUsername(this.robUsername).block()).isNotNull(); + } + + @Test + public void findByUsernameWhenUsernameDoesNotMatchThenFound() { + assertThat(repository.findByUsername(this.robUsername + "NOTFOUND").block()).isNull(); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java new file mode 100644 index 0000000000..d7d29d284e --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java @@ -0,0 +1,115 @@ +/* + * 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.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.UserDetailsRepositoryAuthenticationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.config.web.server.AuthorizeExchangeBuilder; +import org.springframework.security.config.web.server.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver; +import org.springframework.security.web.server.authorization.AuthorizationContext; +import org.springframework.security.web.server.context.WebSessionSecurityContextRepository; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; +import org.springframework.web.server.WebFilter; +import reactor.core.publisher.Mono; +import reactor.ipc.netty.NettyContext; +import reactor.ipc.netty.http.server.HttpServer; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.config.web.server.HttpSecurity.http; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Configuration +@EnableWebFlux +@ComponentScan +public class HelloWebfluxFnApplication { + @Value("${server.port:8080}") + private int port = 8080; + + public static void main(String[] args) throws Exception { + try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HelloWebfluxFnApplication.class)) { + context.getBean(NettyContext.class).onClose().block(); + } + } + + @Bean + public NettyContext nettyContext(UserController userController, WebFilter springSecurityFilterChain) { + RouterFunction route = route( + GET("/principal"), userController::principal).andRoute( + GET("/users"), userController::users).andRoute( + GET("/admin"), userController::admin); + + HandlerStrategies handlerStrategies = HandlerStrategies.builder() + .webFilter(springSecurityFilterChain).build(); + + HttpHandler handler = RouterFunctions.toHttpHandler(route, handlerStrategies); + ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); + HttpServer httpServer = HttpServer.create("localhost", port); + return httpServer.newHandler(adapter).block(); + } + + @Bean + WebFilter springSecurityFilterChain(ReactiveAuthenticationManager manager) throws Exception { + HttpSecurity http = http(); + http.securityContextRepository(new WebSessionSecurityContextRepository()); + http.authenticationManager(manager); + http.httpBasic(); + + AuthorizeExchangeBuilder authorize = http.authorizeExchange(); + authorize.antMatchers("/admin/**").hasRole("ADMIN"); + authorize.antMatchers("/users/{user}/**").access(this::currentUserMatchesPath); + authorize.anyExchange().authenticated(); + return http.build(); + } + + private Mono currentUserMatchesPath(Mono authentication, AuthorizationContext context) { + return authentication + .map( a -> context.getVariables().get("user").equals(a.getName())) + .map( granted -> new AuthorizationDecision(granted)); + } + + @Bean + public ReactiveAuthenticationManager authenticationManager(UserRepositoryUserDetailsRepository udr) { + return new UserDetailsRepositoryAuthenticationManager(udr); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/MapUserRepository.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/MapUserRepository.java new file mode 100644 index 0000000000..ed775a1b33 --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/MapUserRepository.java @@ -0,0 +1,58 @@ +/* + * + * * 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 java.util.HashMap; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Service +public class MapUserRepository implements UserRepository { + private final Map users = new HashMap<>(); + + public MapUserRepository() { + save(new User("rob", "rob", "Rob", "Winch")).block(); + save(new User("admin", "admin", "Admin", "User")).block(); + } + + @Override + public Flux findAll() { + return Flux.fromIterable(users.values()); + } + + @Override + public Mono findByUsername(String username) { + User result = users.get(username); + + return result == null ? Mono.empty() : Mono.just(result); + } + + public Mono save(User user) { + users.put(user.getUsername(), user); + return Mono.just(user); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/User.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/User.java new file mode 100644 index 0000000000..ef81772c6b --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/User.java @@ -0,0 +1,84 @@ +/* + * 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; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class User { + + private Long id; + private String username; + private String password; + private String firstname; + private String lastname; + + public User() {} + + public User(User copy) { + this(copy.getUsername(), copy.getPassword(), copy.getFirstname(), copy.getLastname()); + } + + public User(String username, String password, String firstname, String lastname) { + super(); + this.username = username; + this.password = password; + this.firstname = firstname; + this.lastname = lastname; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserController.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserController.java new file mode 100644 index 0000000000..cadc1d6e22 --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserController.java @@ -0,0 +1,65 @@ +/* + * 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.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.context.SecurityContextRepository; +import org.springframework.security.web.server.context.WebSessionSecurityContextRepository; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.Map; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Component +public class UserController { + private final SecurityContextRepository repo = new WebSessionSecurityContextRepository(); + + private final UserRepository users; + + public UserController(UserRepository users) { + this.users = users; + } + + public Mono principal(ServerRequest serverRequest) { + return serverRequest.principal().cast(Authentication.class).flatMap(p -> + ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .syncBody(p.getPrincipal())); + } + + public Mono users(ServerRequest serverRequest) { + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(this.users.findAll(), User.class); + } + + public Mono admin(ServerRequest serverRequest) { + return serverRequest.principal().cast(Authentication.class).flatMap(p -> + ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .syncBody( Collections.singletonMap("isadmin", "true"))); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepository.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepository.java new file mode 100644 index 0000000000..58fd96b437 --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepository.java @@ -0,0 +1,33 @@ +/* + * 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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Rob Winch + * @since 5.0 + */ +public interface UserRepository { + + Flux findAll(); + + Mono findByUsername(String username); + + Mono save(User user); +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepositoryUserDetailsRepository.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepositoryUserDetailsRepository.java new file mode 100644 index 0000000000..d475e7b10b --- /dev/null +++ b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/UserRepositoryUserDetailsRepository.java @@ -0,0 +1,87 @@ +/* + * 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 java.util.Collection; +import java.util.List; + +import org.springframework.security.authentication.UserDetailsRepository; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.0 + */ +@Component +public class UserRepositoryUserDetailsRepository implements UserDetailsRepository { + private final UserRepository users; + + public UserRepositoryUserDetailsRepository(UserRepository users) { + super(); + this.users = users; + } + + @Override + public Mono findByUsername(String username) { + return this.users + .findByUsername(username) + .map(UserDetailsAdapter::new); + } + + @SuppressWarnings("serial") + private static class UserDetailsAdapter extends User implements UserDetails { + private static List USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER"); + private static List ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER"); + + private UserDetailsAdapter(User delegate) { + super(delegate); + } + + @Override + public Collection getAuthorities() { + return isAdmin() ? ADMIN_ROLES : USER_ROLES ; + } + + private boolean isAdmin() { + return getUsername().contains("admin"); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + } +}