JAVA-29298: Update spring-security-opa to parent-boot-3. (#15827)
This commit is contained in:
parent
831acaf8cf
commit
3cd8d990e3
@ -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:
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<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>
|
||||||
@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
<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>
|
||||||
|
@ -17,8 +17,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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";
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
.and()
|
|
||||||
.authorizeExchange(exchanges -> {
|
|
||||||
exchanges
|
|
||||||
.pathMatchers("/account/*")
|
|
||||||
.access(opaAuthManager(opaWebClient));
|
|
||||||
})
|
|
||||||
.build();
|
.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) -> {
|
|
||||||
return opaWebClient.post()
|
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(toAuthorizationPayload(auth,context), Map.class)
|
.body(toAuthorizationPayload(auth, context), Map.class)
|
||||||
.exchangeToMono(this::toDecision);
|
.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").path("authorized").asBoolean(false);
|
boolean authorized = node.path("result")
|
||||||
|
.path("authorized")
|
||||||
|
.asBoolean(false);
|
||||||
return new AuthorizationDecision(authorized);
|
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()
|
||||||
Map<String,String> headers = context.getExchange().getRequest()
|
|
||||||
.getHeaders()
|
.getHeaders()
|
||||||
.toSingleValueMap();
|
.toSingleValueMap();
|
||||||
|
|
||||||
Map<String,Object> attributes = ImmutableMap.<String,Object>builder()
|
Map<String, Object> attributes = ImmutableMap.<String, Object> builder()
|
||||||
.put("principal",a.getName())
|
.put("principal", a.getName())
|
||||||
.put("authorities",
|
.put("authorities", a.getAuthorities()
|
||||||
a.getAuthorities()
|
|
||||||
.stream()
|
.stream()
|
||||||
.map(g -> g.getAuthority())
|
.map(GrantedAuthority::getAuthority)
|
||||||
.collect(Collectors.toList()))
|
.collect(Collectors.toList()))
|
||||||
.put("uri", context.getExchange().getRequest().getURI().getPath())
|
.put("uri", context.getExchange()
|
||||||
.put("headers",headers)
|
.getRequest()
|
||||||
|
.getURI()
|
||||||
|
.getPath())
|
||||||
|
.put("headers", headers)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Map<String,Object> input = ImmutableMap.<String,Object>builder()
|
return ImmutableMap.<String, Object> builder()
|
||||||
.put("input",attributes)
|
.put("input", attributes)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return input;
|
|
||||||
});
|
});
|
||||||
// @formatter:on
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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,14 +10,10 @@ 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"))
|
||||||
@ -28,7 +21,6 @@ public class AccountService {
|
|||||||
.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")));
|
||||||
|
@ -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 {
|
||||||
@ -29,9 +28,8 @@ class AccountControllerLiveTest {
|
|||||||
.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")
|
||||||
@ -42,7 +40,7 @@ class AccountControllerLiveTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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")
|
||||||
@ -53,7 +51,7 @@ class AccountControllerLiveTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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")
|
||||||
@ -63,5 +61,4 @@ class AccountControllerLiveTest {
|
|||||||
.isForbidden();
|
.isForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user