diff --git a/spring-security-modules/spring-security-opa/README.md b/spring-security-modules/spring-security-opa/README.md index d2c1652edb..fc50304073 100644 --- a/spring-security-modules/spring-security-opa/README.md +++ b/spring-security-modules/spring-security-opa/README.md @@ -1,3 +1,4 @@ +Note: For integration testing get the OPA server running first. Check the official [OPA documentation](https://www.openpolicyagent.org/docs/latest/) for instructions on how to run the OPA server. ### Relevant Articles: diff --git a/spring-security-modules/spring-security-opa/pom.xml b/spring-security-modules/spring-security-opa/pom.xml index 81c4709124..05d135947b 100644 --- a/spring-security-modules/spring-security-opa/pom.xml +++ b/spring-security-modules/spring-security-opa/pom.xml @@ -1,14 +1,15 @@ - + 4.0.0 spring-security-opa Spring Security with OPA authorization com.baeldung - spring-security-modules + parent-boot-3 0.0.1-SNAPSHOT + ../../parent-boot-3 @@ -28,7 +29,7 @@ com.google.guava guava - 31.0.1-jre + ${guava.version} org.springframework.boot diff --git a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/Application.java b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/Application.java index 789a1f803f..a698630f3b 100644 --- a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/Application.java +++ b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/Application.java @@ -5,7 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { - + public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaConfiguration.java b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaConfiguration.java index e24fdbcf35..2a96bef606 100644 --- a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaConfiguration.java +++ b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaConfiguration.java @@ -11,15 +11,14 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @EnableConfigurationProperties(OpaProperties.class) public class OpaConfiguration { - + private final OpaProperties opaProperties; - + @Bean public WebClient opaWebClient(WebClient.Builder builder) { - - return builder - .baseUrl(opaProperties.getEndpoint()) - .build(); + + return builder.baseUrl(opaProperties.getEndpoint()) + .build(); } } diff --git a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaProperties.java b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaProperties.java index acc23a2fd2..0a12f4d772 100644 --- a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaProperties.java +++ b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/OpaProperties.java @@ -1,14 +1,14 @@ package com.baeldung.security.opa.config; -import javax.annotation.Nonnull; - import org.springframework.boot.context.properties.ConfigurationProperties; +import jakarta.annotation.Nonnull; import lombok.Data; @ConfigurationProperties(prefix = "opa") @Data public class OpaProperties { + @Nonnull private String endpoint = "http://localhost:8181"; } diff --git a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/SecurityConfiguration.java b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/SecurityConfiguration.java index 7e10cb2e8a..98de220e21 100644 --- a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/SecurityConfiguration.java +++ b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/config/SecurityConfiguration.java @@ -1,8 +1,6 @@ package com.baeldung.security.opa.config; -import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @@ -11,17 +9,16 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; -import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authorization.AuthorizationContext; -import org.springframework.web.reactive.function.BodyInserter; -import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; @@ -32,79 +29,65 @@ import reactor.core.publisher.Mono; @Configuration public class SecurityConfiguration { - - + @Bean - public SecurityWebFilterChain accountAuthorization(ServerHttpSecurity http, @Qualifier("opaWebClient")WebClient opaWebClient) { - - // @formatter:on - return http - .httpBasic() - .and() - .authorizeExchange(exchanges -> { - exchanges - .pathMatchers("/account/*") - .access(opaAuthManager(opaWebClient)); - }) - .build(); - // @formatter:on - + public SecurityWebFilterChain accountAuthorization(ServerHttpSecurity http, @Qualifier("opaWebClient") WebClient opaWebClient) { + return http.httpBasic(Customizer.withDefaults()) + .authorizeExchange(exchanges -> exchanges.pathMatchers("/account/*") + .access(opaAuthManager(opaWebClient))) + .build(); } @Bean public ReactiveAuthorizationManager opaAuthManager(WebClient opaWebClient) { - - return (auth, context) -> { - return opaWebClient.post() - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .body(toAuthorizationPayload(auth,context), Map.class) - .exchangeToMono(this::toDecision); - }; + return (auth, context) -> opaWebClient.post() + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .body(toAuthorizationPayload(auth, context), Map.class) + .exchangeToMono(this::toDecision); } private Mono toDecision(ClientResponse response) { - - if ( !response.statusCode().is2xxSuccessful()) { + if (!response.statusCode() + .is2xxSuccessful()) { return Mono.just(new AuthorizationDecision(false)); } - - return response - .bodyToMono(ObjectNode.class) - .map(node -> { - boolean authorized = node.path("result").path("authorized").asBoolean(false); - return new AuthorizationDecision(authorized); - }); - + + return response.bodyToMono(ObjectNode.class) + .map(node -> { + boolean authorized = node.path("result") + .path("authorized") + .asBoolean(false); + return new AuthorizationDecision(authorized); + }); + } - private Publisher> toAuthorizationPayload(Mono auth, AuthorizationContext context) { - // @formatter:off - return auth - .defaultIfEmpty(new AnonymousAuthenticationToken("**ANONYMOUS**", new Object(), Arrays.asList(new SimpleGrantedAuthority("ANONYMOUS")))) - .map( a -> { - - Map headers = context.getExchange().getRequest() - .getHeaders() - .toSingleValueMap(); - - Map attributes = ImmutableMap.builder() - .put("principal",a.getName()) - .put("authorities", - a.getAuthorities() - .stream() - .map(g -> g.getAuthority()) - .collect(Collectors.toList())) - .put("uri", context.getExchange().getRequest().getURI().getPath()) - .put("headers",headers) - .build(); - - Map input = ImmutableMap.builder() - .put("input",attributes) - .build(); - - return input; - }); - // @formatter:on + private Publisher> toAuthorizationPayload(Mono auth, AuthorizationContext context) { + return auth.defaultIfEmpty( + new AnonymousAuthenticationToken("**ANONYMOUS**", new Object(), Collections.singletonList(new SimpleGrantedAuthority("ANONYMOUS")))) + .map(a -> { + Map headers = context.getExchange() + .getRequest() + .getHeaders() + .toSingleValueMap(); + + Map attributes = ImmutableMap. builder() + .put("principal", a.getName()) + .put("authorities", a.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList())) + .put("uri", context.getExchange() + .getRequest() + .getURI() + .getPath()) + .put("headers", headers) + .build(); + + return ImmutableMap. builder() + .put("input", attributes) + .build(); + }); } } diff --git a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/controller/AccountController.java b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/controller/AccountController.java index ba0ff61554..cc8f62454a 100644 --- a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/controller/AccountController.java +++ b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/controller/AccountController.java @@ -13,9 +13,9 @@ import reactor.core.publisher.Mono; @RestController @RequiredArgsConstructor public class AccountController { - + private final AccountService accountService; - + @GetMapping("/account/{accountId}") public Mono getAccount(@PathVariable("accountId") String accountId) { return accountService.findByAccountId(accountId); diff --git a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/domain/Account.java b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/domain/Account.java index db494627a8..c757c742c5 100644 --- a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/domain/Account.java +++ b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/domain/Account.java @@ -6,20 +6,17 @@ import lombok.Data; @Data public class Account { - + private String id; private BigDecimal balance; private String currency; - - + public static Account of(String id, BigDecimal balance, String currency) { - Account acc = new Account(); - acc.setId(id); - acc.setBalance(balance); - acc.setCurrency(currency); - - return acc; + Account account = new Account(); + account.setId(id); + account.setBalance(balance); + account.setCurrency(currency); + return account; } - } diff --git a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/service/AccountService.java b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/service/AccountService.java index 18968019f9..e98f120fa1 100644 --- a/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/service/AccountService.java +++ b/spring-security-modules/spring-security-opa/src/main/java/com/baeldung/security/opa/service/AccountService.java @@ -1,6 +1,3 @@ -/** - * - */ package com.baeldung.security.opa.service; import java.math.BigDecimal; @@ -13,25 +10,20 @@ import com.google.common.collect.ImmutableMap; import reactor.core.publisher.Mono; -/** - * @author Philippe - * - */ @Service public class AccountService { - - private Map accounts = ImmutableMap.builder() - .put("0001", Account.of("0001", BigDecimal.valueOf(100.00), "USD")) - .put("0002", Account.of("0002", BigDecimal.valueOf(101.00), "EUR")) - .put("0003", Account.of("0003", BigDecimal.valueOf(102.00), "BRL")) - .put("0004", Account.of("0004", BigDecimal.valueOf(103.00), "AUD")) - .put("0005", Account.of("0005", BigDecimal.valueOf(10400.00), "JPY")) - .build(); - - + + private Map accounts = ImmutableMap. builder() + .put("0001", Account.of("0001", BigDecimal.valueOf(100.00), "USD")) + .put("0002", Account.of("0002", BigDecimal.valueOf(101.00), "EUR")) + .put("0003", Account.of("0003", BigDecimal.valueOf(102.00), "BRL")) + .put("0004", Account.of("0004", BigDecimal.valueOf(103.00), "AUD")) + .put("0005", Account.of("0005", BigDecimal.valueOf(10400.00), "JPY")) + .build(); + public Mono findByAccountId(String accountId) { return Mono.just(accounts.get(accountId)) - .switchIfEmpty(Mono.error(new IllegalArgumentException("invalid.account"))); + .switchIfEmpty(Mono.error(new IllegalArgumentException("invalid.account"))); } } diff --git a/spring-security-modules/spring-security-opa/src/test/java/com/baeldung/security/opa/controller/AccountControllerLiveTest.java b/spring-security-modules/spring-security-opa/src/test/java/com/baeldung/security/opa/controller/AccountControllerLiveTest.java index 7469fd327c..2b18b04c70 100644 --- a/spring-security-modules/spring-security-opa/src/test/java/com/baeldung/security/opa/controller/AccountControllerLiveTest.java +++ b/spring-security-modules/spring-security-opa/src/test/java/com/baeldung/security/opa/controller/AccountControllerLiveTest.java @@ -12,7 +12,6 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.reactive.server.WebTestClient; -// !!! NOTICE: Start OPA server before running this test class !!! @SpringBootTest @ActiveProfiles("test") class AccountControllerLiveTest { @@ -24,44 +23,42 @@ class AccountControllerLiveTest { @BeforeEach public void setup() { this.rest = WebTestClient.bindToApplicationContext(this.context) - .apply(springSecurity()) - .configureClient() - .build(); + .apply(springSecurity()) + .configureClient() + .build(); } - @Test - @WithMockUser(username = "user1", roles = { "account:read:0001"} ) + @WithMockUser(username = "user1", roles = { "account:read:0001" }) void testGivenValidUser_thenSuccess() { rest.get() - .uri("/account/0001") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus() - .is2xxSuccessful(); + .uri("/account/0001") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .is2xxSuccessful(); } @Test - @WithMockUser(username = "user1", roles = { "account:read:0002"} ) + @WithMockUser(username = "user1", roles = { "account:read:0002" }) void testGivenValidUser_thenUnauthorized() { rest.get() - .uri("/account/0001") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus() - .isForbidden(); + .uri("/account/0001") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isForbidden(); } - + @Test - @WithMockUser(username = "user1", roles = {} ) + @WithMockUser(username = "user1", roles = {}) void testGivenNoAuthorities_thenForbidden() { rest.get() - .uri("/account/0001") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus() - .isForbidden(); + .uri("/account/0001") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isForbidden(); } - - + }