Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix access rules order and add integration tests #16

Merged
merged 2 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions gateway/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@
<imageTag>${project.version}</imageTag>
<spring-boot.build-image.imageName>georchestra/gateway:${imageTag}</spring-boot.build-image.imageName>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<!-- override provided version 2.26.3 -->
<version>2.33.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.georchestra</groupId>
Expand Down Expand Up @@ -70,13 +80,33 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.springframework.security</groupId> -->
<!-- <artifactId>spring-security-test</artifactId> -->
<!-- <scope>test</scope> -->
<!-- </dependency> -->
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<!-- override provided version 2.22.2 -->
<version>3.0.0-M6</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>net.revelc.code.formatter</groupId>
Expand Down Expand Up @@ -174,6 +204,18 @@
<flattenMode>oss</flattenMode>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
Expand All @@ -189,7 +231,6 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<id>prepare-agent</id>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.web.server.ServerWebExchange;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

/**
Expand All @@ -44,7 +45,7 @@
* @see GeorchestraUserHeadersContributor
* @see GeorchestraOrganizationHeadersContributor
*/
@Slf4j
@Slf4j(topic = "org.georchestra.gateway.filter.headers")
public abstract class HeaderContributor implements Ordered {

/**
Expand All @@ -69,25 +70,37 @@ public abstract class HeaderContributor implements Ordered {
return 0;
}

protected void add(HttpHeaders target, String header, Optional<Boolean> enabled, Optional<String> value) {
protected void add(@NonNull HttpHeaders target, @NonNull String header, @NonNull Optional<Boolean> enabled,
@NonNull Optional<String> value) {
add(target, header, enabled, value.orElse(null));
}

protected void add(HttpHeaders target, String header, Optional<Boolean> enabled, List<String> values) {
add(target, header, enabled, values.stream().collect(Collectors.joining(";")));
protected void add(@NonNull HttpHeaders target, @NonNull String header, @NonNull Optional<Boolean> enabled,
@NonNull List<String> values) {
String val = values.isEmpty() ? null : values.stream().collect(Collectors.joining(";"));
add(target, header, enabled, val);
}

protected void add(HttpHeaders target, String header, Optional<Boolean> enabled, String value) {
protected void add(@NonNull HttpHeaders target, @NonNull String header, @NonNull Optional<Boolean> enabled,
String value) {
if (enabled.orElse(Boolean.FALSE).booleanValue()) {
if (null == value) {
log.debug("Value for header {} is not present", header);
log.trace("Value for header {} is not present", header);
} else {
log.info("Appending header {}: {}", header, value);
log.debug("Appending header {}: {}", header, value);
target.add(header, value);
}
} else {
log.debug("Header {} is not enabled", header);
log.trace("Header {} is not enabled", header);
}
}

protected void add(@NonNull HttpHeaders target, @NonNull String header, String value) {
if (null == value) {
log.trace("Value for header {} is not present", header);
} else {
log.debug("Appending header {}: {}", header, value);
target.add(header, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
package org.georchestra.gateway.filter.headers;

import java.util.List;
import java.util.function.BooleanSupplier;

import org.georchestra.gateway.filter.headers.providers.GeorchestraOrganizationHeadersContributor;
import org.georchestra.gateway.filter.headers.providers.GeorchestraUserHeadersContributor;
import org.georchestra.gateway.filter.headers.providers.JsonPayloadHeadersContributor;
import org.georchestra.gateway.filter.headers.providers.SecProxyHeaderContributor;
import org.georchestra.gateway.model.GatewayConfigProperties;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -51,8 +53,9 @@ public class HeaderFiltersConfiguration {
return new GeorchestraUserHeadersContributor();
}

public @Bean SecProxyHeaderContributor secProxyHeaderProvider() {
return new SecProxyHeaderContributor();
public @Bean SecProxyHeaderContributor secProxyHeaderProvider(GatewayConfigProperties configProps) {
BooleanSupplier secProxyEnabledSupplier = () -> configProps.getDefaultHeaders().getProxy().orElse(false);
return new SecProxyHeaderContributor(secProxyEnabledSupplier);
}

public @Bean GeorchestraOrganizationHeadersContributor organizationSecurityHeadersProvider() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* </pre>
*
*/
@Slf4j(topic = "org.georchestra.gateway.headers")
@Slf4j(topic = "org.georchestra.gateway.filter.headers")
public class RemoveHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory<RegExConfig> {

public RemoveHeadersGatewayFilterFactory() {
Expand Down Expand Up @@ -119,7 +119,7 @@ boolean anyMatches(@NonNull HttpHeaders headers) {
void removeMatching(@NonNull HttpHeaders headers) {
new HashSet<>(headers.keySet()).stream()//
.filter(this::matches)//
.peek(name -> log.debug("Removing header {}", name))//
.peek(name -> log.trace("Removing header {}", name))//
.forEach(headers::remove);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,30 @@
*/
package org.georchestra.gateway.filter.headers.providers;

import java.util.function.BooleanSupplier;
import java.util.function.Consumer;

import org.georchestra.gateway.filter.headers.HeaderContributor;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;

/**
* Unconditionally contributes the {@literal sec-proxy: true} request header,
* which is required by all backend services as a flag indicating the request is
* authenticated.
* Contributes the {@literal sec-proxy: true} request header based on the value
* returned by the provided {@link BooleanSupplier}, which is required by all
* backend services as a flag indicating the request is authenticated.
*/
@RequiredArgsConstructor
public class SecProxyHeaderContributor extends HeaderContributor {

private final @NonNull BooleanSupplier secProxyHeaderEnabled;

public @Override Consumer<HttpHeaders> prepare(ServerWebExchange exchange) {
return headers -> {
headers.add("sec-proxy", "true");
if (secProxyHeaderEnabled.getAsBoolean())
add(headers, "sec-proxy", "true");
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public class RoleBasedAccessRule {
*/
private List<String> interceptUrl = List.of();

/**
* Highest precedence rule, if {@code true}, forbids access to the intercepted
* URLs
*/
private boolean forbidden = false;

/**
* Whether anonymous (unauthenticated) access is to be granted to the
* intercepted URIs. If {@code true}, no further specification is applied to the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
* @see GeorchestraUserMapper
*/
@RequiredArgsConstructor
@Slf4j
@Slf4j(topic = "org.georchestra.gateway.security")
public class ResolveGeorchestraUserGlobalFilter implements GlobalFilter, Ordered {

public static final int ORDER = RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,22 @@ public void customize(ServerHttpSecurity http) {

AuthorizeExchangeSpec authorizeExchange = http.authorizeExchange();

log.info("Applying global access rules...");
apply(authorizeExchange, config.getGlobalAccessRules());
// apply service-specific rules before global rules, order matters, and
// otherwise global path matches would be applied before service ones.

config.getServices().forEach((name, service) -> {
log.info("Applying access rules for backend service '{}'", name);
apply(authorizeExchange, service.getAccessRules());
apply(name, authorizeExchange, service.getAccessRules());
});

log.info("Applying global access rules...");
apply("global", authorizeExchange, config.getGlobalAccessRules());
}

private void apply(AuthorizeExchangeSpec authorizeExchange, List<RoleBasedAccessRule> accessRules) {
private void apply(String serviceName, AuthorizeExchangeSpec authorizeExchange,
List<RoleBasedAccessRule> accessRules) {
if (accessRules == null || accessRules.isEmpty()) {
log.info("No access rules found.");
log.debug("No {} access rules found.", serviceName);
return;
}
for (RoleBasedAccessRule rule : accessRules) {
Expand All @@ -83,18 +87,22 @@ private void apply(AuthorizeExchangeSpec authorizeExchange, List<RoleBasedAccess
@VisibleForTesting
void apply(AuthorizeExchangeSpec authorizeExchange, RoleBasedAccessRule rule) {
final List<String> antPatterns = resolveAntPatterns(rule);
final boolean forbidden = rule.isForbidden();
final boolean anonymous = rule.isAnonymous();
final List<String> allowedRoles = rule.getAllowedRoles() == null ? List.of() : rule.getAllowedRoles();
Access access = authorizeExchange(authorizeExchange, antPatterns);
if (anonymous) {
if (forbidden) {
log.debug("Denying access to everyone for {}", antPatterns);
denyAll(access);
} else if (anonymous) {
log.debug("Granting anonymous access for {}", antPatterns);
permitAll(access);
} else if (allowedRoles.isEmpty()) {
log.debug("Granting access to any authenticated user for {}", antPatterns);
requireAuthenticatedUser(access);
} else {
List<String> roles = resolveRoles(antPatterns, allowedRoles);
log.debug("Granting access to roles {} for {}", antPatterns);
log.debug("Granting access to roles {} for {}", roles, antPatterns);
hasAnyAuthority(access, roles);
}
}
Expand All @@ -115,10 +123,7 @@ Access authorizeExchange(AuthorizeExchangeSpec authorizeExchange, List<String> a
}

private List<String> resolveRoles(List<String> antPatterns, List<String> allowedRoles) {
List<String> roles = allowedRoles.stream().map(this::ensureRolePrefix).collect(Collectors.toList());
if (log.isDebugEnabled())
log.debug("Access rule: {} has any role: {}", antPatterns, roles.stream().collect(Collectors.joining(",")));
return roles;
return allowedRoles.stream().map(this::ensureRolePrefix).collect(Collectors.toList());
}

@VisibleForTesting
Expand All @@ -136,6 +141,11 @@ void permitAll(Access access) {
access.permitAll();
}

@VisibleForTesting
void denyAll(Access access) {
access.denyAll();
}

private String ensureRolePrefix(@NonNull String roleName) {
return roleName.startsWith("ROLE_") ? roleName : ("ROLE_" + roleName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
* @author groldan
*
*/
@Slf4j
@Slf4j(topic = "org.georchestra.gateway.security.oauth2")
public class OAuth2AuthenticationTokenOpenIDUserMapper extends OAuth2AuthenticationTokenUserMapper {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.georchestra.gateway.model.GeorchestraUsers;
import org.georchestra.gateway.security.GeorchestraUserMapperExtension;
import org.georchestra.security.model.GeorchestraUser;
import org.slf4j.Logger;
Expand All @@ -42,7 +41,7 @@
* @author groldan
*
*/
@Slf4j
@Slf4j(topic = "org.georchestra.gateway.security.oauth2")
public class OAuth2AuthenticationTokenUserMapper implements GeorchestraUserMapperExtension {

@Override
Expand Down
10 changes: 8 additions & 2 deletions gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,14 @@ logging:
root: warn
'[org.springframework]': info
'[org.springframework.cloud.gateway]': info
'[org.springframework.security]': debug
'[org.springframework.security]': info
'[org.springframework.security.oauth2]': debug
'[reactor.netty.http ]': debug
'[org.georchestra.gateway]': debug
'[org.georchestra.gateway]': info
'[org.georchestra.gateway.filter.headers]': debug
'[org.georchestra.gateway.config.security]': debug
'[org.georchestra.gateway.config.security.accessrules]': debug
'[org.georchestra.gateway.security.ldap]': debug
'[org.georchestra.gateway.security.oauth2]': debug


Loading