JAVA-29298: Update spring-security-opa to parent-boot-3. (#15827)

This commit is contained in:
Harry9656 2024-02-12 21:15:52 +01:00 committed by GitHub
parent 831acaf8cf
commit 3cd8d990e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 107 additions and 137 deletions

View File

@ -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: ### Relevant Articles:

View File

@ -1,14 +1,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-opa</artifactId> <artifactId>spring-security-opa</artifactId>
<description>Spring Security with OPA authorization</description> <description>Spring Security with OPA authorization</description>
<parent> <parent>
<groupId>com.baeldung</groupId> <groupId>com.baeldung</groupId>
<artifactId>spring-security-modules</artifactId> <artifactId>parent-boot-3</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-3</relativePath>
</parent> </parent>
<dependencies> <dependencies>
@ -28,7 +29,7 @@
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
<version>31.0.1-jre</version> <version>${guava.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -17,9 +17,8 @@ public class OpaConfiguration {
@Bean @Bean
public WebClient opaWebClient(WebClient.Builder builder) { public WebClient opaWebClient(WebClient.Builder builder) {
return builder return builder.baseUrl(opaProperties.getEndpoint())
.baseUrl(opaProperties.getEndpoint()) .build();
.build();
} }
} }

View File

@ -1,14 +1,14 @@
package com.baeldung.security.opa.config; package com.baeldung.security.opa.config;
import javax.annotation.Nonnull;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import jakarta.annotation.Nonnull;
import lombok.Data; import lombok.Data;
@ConfigurationProperties(prefix = "opa") @ConfigurationProperties(prefix = "opa")
@Data @Data
public class OpaProperties { public class OpaProperties {
@Nonnull @Nonnull
private String endpoint = "http://localhost:8181"; private String endpoint = "http://localhost:8181";
} }

View File

@ -1,8 +1,6 @@
package com.baeldung.security.opa.config; package com.baeldung.security.opa.config;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authorization.AuthorizationContext; 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.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
@ -33,78 +30,64 @@ import reactor.core.publisher.Mono;
@Configuration @Configuration
public class SecurityConfiguration { public class SecurityConfiguration {
@Bean @Bean
public SecurityWebFilterChain accountAuthorization(ServerHttpSecurity http, @Qualifier("opaWebClient")WebClient opaWebClient) { public SecurityWebFilterChain accountAuthorization(ServerHttpSecurity http, @Qualifier("opaWebClient") WebClient opaWebClient) {
return http.httpBasic(Customizer.withDefaults())
// @formatter:on .authorizeExchange(exchanges -> exchanges.pathMatchers("/account/*")
return http .access(opaAuthManager(opaWebClient)))
.httpBasic() .build();
.and()
.authorizeExchange(exchanges -> {
exchanges
.pathMatchers("/account/*")
.access(opaAuthManager(opaWebClient));
})
.build();
// @formatter:on
} }
@Bean @Bean
public ReactiveAuthorizationManager<AuthorizationContext> opaAuthManager(WebClient opaWebClient) { public ReactiveAuthorizationManager<AuthorizationContext> opaAuthManager(WebClient opaWebClient) {
return (auth, context) -> opaWebClient.post()
return (auth, context) -> { .accept(MediaType.APPLICATION_JSON)
return opaWebClient.post() .contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON) .body(toAuthorizationPayload(auth, context), Map.class)
.contentType(MediaType.APPLICATION_JSON) .exchangeToMono(this::toDecision);
.body(toAuthorizationPayload(auth,context), Map.class)
.exchangeToMono(this::toDecision);
};
} }
private Mono<AuthorizationDecision> toDecision(ClientResponse response) { private Mono<AuthorizationDecision> toDecision(ClientResponse response) {
if (!response.statusCode()
if ( !response.statusCode().is2xxSuccessful()) { .is2xxSuccessful()) {
return Mono.just(new AuthorizationDecision(false)); return Mono.just(new AuthorizationDecision(false));
} }
return response return response.bodyToMono(ObjectNode.class)
.bodyToMono(ObjectNode.class) .map(node -> {
.map(node -> { boolean authorized = node.path("result")
boolean authorized = node.path("result").path("authorized").asBoolean(false); .path("authorized")
return new AuthorizationDecision(authorized); .asBoolean(false);
}); return new AuthorizationDecision(authorized);
});
} }
private Publisher<Map<String,Object>> toAuthorizationPayload(Mono<Authentication> auth, AuthorizationContext context) { private Publisher<Map<String, Object>> toAuthorizationPayload(Mono<Authentication> auth, AuthorizationContext context) {
// @formatter:off return auth.defaultIfEmpty(
return auth new AnonymousAuthenticationToken("**ANONYMOUS**", new Object(), Collections.singletonList(new SimpleGrantedAuthority("ANONYMOUS"))))
.defaultIfEmpty(new AnonymousAuthenticationToken("**ANONYMOUS**", new Object(), Arrays.asList(new SimpleGrantedAuthority("ANONYMOUS")))) .map(a -> {
.map( a -> { Map<String, String> headers = context.getExchange()
.getRequest()
.getHeaders()
.toSingleValueMap();
Map<String,String> headers = context.getExchange().getRequest() Map<String, Object> attributes = ImmutableMap.<String, Object> builder()
.getHeaders() .put("principal", a.getName())
.toSingleValueMap(); .put("authorities", a.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.put("uri", context.getExchange()
.getRequest()
.getURI()
.getPath())
.put("headers", headers)
.build();
Map<String,Object> attributes = ImmutableMap.<String,Object>builder() return ImmutableMap.<String, Object> builder()
.put("principal",a.getName()) .put("input", attributes)
.put("authorities", .build();
a.getAuthorities() });
.stream()
.map(g -> g.getAuthority())
.collect(Collectors.toList()))
.put("uri", context.getExchange().getRequest().getURI().getPath())
.put("headers",headers)
.build();
Map<String,Object> input = ImmutableMap.<String,Object>builder()
.put("input",attributes)
.build();
return input;
});
// @formatter:on
} }
} }

View File

@ -11,15 +11,12 @@ public class Account {
private BigDecimal balance; private BigDecimal balance;
private String currency; private String currency;
public static Account of(String id, BigDecimal balance, String currency) { public static Account of(String id, BigDecimal balance, String currency) {
Account acc = new Account(); Account account = new Account();
acc.setId(id); account.setId(id);
acc.setBalance(balance); account.setBalance(balance);
acc.setCurrency(currency); account.setCurrency(currency);
return account;
return acc;
} }
} }

View File

@ -1,6 +1,3 @@
/**
*
*/
package com.baeldung.security.opa.service; package com.baeldung.security.opa.service;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -13,25 +10,20 @@ import com.google.common.collect.ImmutableMap;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/**
* @author Philippe
*
*/
@Service @Service
public class AccountService { public class AccountService {
private Map<String, Account> accounts = ImmutableMap.<String, Account>builder() private Map<String, Account> accounts = ImmutableMap.<String, Account> builder()
.put("0001", Account.of("0001", BigDecimal.valueOf(100.00), "USD")) .put("0001", Account.of("0001", BigDecimal.valueOf(100.00), "USD"))
.put("0002", Account.of("0002", BigDecimal.valueOf(101.00), "EUR")) .put("0002", Account.of("0002", BigDecimal.valueOf(101.00), "EUR"))
.put("0003", Account.of("0003", BigDecimal.valueOf(102.00), "BRL")) .put("0003", Account.of("0003", BigDecimal.valueOf(102.00), "BRL"))
.put("0004", Account.of("0004", BigDecimal.valueOf(103.00), "AUD")) .put("0004", Account.of("0004", BigDecimal.valueOf(103.00), "AUD"))
.put("0005", Account.of("0005", BigDecimal.valueOf(10400.00), "JPY")) .put("0005", Account.of("0005", BigDecimal.valueOf(10400.00), "JPY"))
.build(); .build();
public Mono<Account> findByAccountId(String accountId) { public Mono<Account> findByAccountId(String accountId) {
return Mono.just(accounts.get(accountId)) return Mono.just(accounts.get(accountId))
.switchIfEmpty(Mono.error(new IllegalArgumentException("invalid.account"))); .switchIfEmpty(Mono.error(new IllegalArgumentException("invalid.account")));
} }
} }

View File

@ -12,7 +12,6 @@ import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
// !!! NOTICE: Start OPA server before running this test class !!!
@SpringBootTest @SpringBootTest
@ActiveProfiles("test") @ActiveProfiles("test")
class AccountControllerLiveTest { class AccountControllerLiveTest {
@ -24,44 +23,42 @@ class AccountControllerLiveTest {
@BeforeEach @BeforeEach
public void setup() { public void setup() {
this.rest = WebTestClient.bindToApplicationContext(this.context) this.rest = WebTestClient.bindToApplicationContext(this.context)
.apply(springSecurity()) .apply(springSecurity())
.configureClient() .configureClient()
.build(); .build();
} }
@Test @Test
@WithMockUser(username = "user1", roles = { "account:read:0001"} ) @WithMockUser(username = "user1", roles = { "account:read:0001" })
void testGivenValidUser_thenSuccess() { void testGivenValidUser_thenSuccess() {
rest.get() rest.get()
.uri("/account/0001") .uri("/account/0001")
.accept(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)
.exchange() .exchange()
.expectStatus() .expectStatus()
.is2xxSuccessful(); .is2xxSuccessful();
} }
@Test @Test
@WithMockUser(username = "user1", roles = { "account:read:0002"} ) @WithMockUser(username = "user1", roles = { "account:read:0002" })
void testGivenValidUser_thenUnauthorized() { void testGivenValidUser_thenUnauthorized() {
rest.get() rest.get()
.uri("/account/0001") .uri("/account/0001")
.accept(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)
.exchange() .exchange()
.expectStatus() .expectStatus()
.isForbidden(); .isForbidden();
} }
@Test @Test
@WithMockUser(username = "user1", roles = {} ) @WithMockUser(username = "user1", roles = {})
void testGivenNoAuthorities_thenForbidden() { void testGivenNoAuthorities_thenForbidden() {
rest.get() rest.get()
.uri("/account/0001") .uri("/account/0001")
.accept(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)
.exchange() .exchange()
.expectStatus() .expectStatus()
.isForbidden(); .isForbidden();
} }
} }