Skip to content

Commit

Permalink
Support appending sec-* headers as configured, resolved from authenti…
Browse files Browse the repository at this point in the history
…cated user
  • Loading branch information
groldan committed Apr 12, 2022
1 parent 149f38e commit fae09e7
Show file tree
Hide file tree
Showing 42 changed files with 1,550 additions and 210 deletions.
72 changes: 72 additions & 0 deletions configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

# Configuration properties

## Configuration object model

```mermaid
classDiagram
GatewayConfigProperties *-- HeaderMappings : defaultHeaders
GatewayConfigProperties *-- "0..*" RoleBasedAccessRule : globalAccessRules
GatewayConfigProperties *-- "0..*" Service
Service *-- HeaderMappings : headers
Service *-- "0..*" RoleBasedAccessRule : accessRules
class GatewayConfigProperties{
Map<String, Service> services
}
class HeaderMappings{
boolean proxy
boolean username
boolean roles
boolean org
boolean orgname
boolean email
boolean firstname
boolean lastname
boolean tel
boolean jsonUser
boolean jsonOrganization
}
class RoleBasedAccessRule{
List~String~ interceptUrl
boolean anonymous
List~String~ allowedRoles
}
class Service{
URL target
}
```

## Example YAML configuration

```yaml
georchestra:
gateway:
default-headers:
proxy: true
username: true
roles: true
org: true
orgname: true
global-access-rules:
- intercept-url: /**
anonymous: true
services:
analytics:
target: http://analytics:8080/analytics/
access-rules:
- intercept-url: /analytics/**
allowed-roles: SUPERUSER, ORGADMIN
atlas:
target: http://atlas:8080/atlas/
console:
target: http://console:8080/console/
access-rules:
- intercept-url:
- /console/public/**
- /console/manager/public/**
anonymous: true
- intercept-url:
- /console/private/**
- /console/manager/**
allowed-roles: SUPERUSER, ORGADMIN
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@
*/
package org.georchestra.gateway.app;

import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.Map;

import org.georchestra.gateway.model.GeorchestraUser;
import org.georchestra.gateway.security.GeorchestraUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.util.unit.DataSize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.server.ServerWebExchange;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
Expand All @@ -41,15 +46,26 @@
public class GeorchestraGatewayApplication {

private @Autowired RouteLocator routeLocator;
private @Autowired GeorchestraUserMapper userMapper;

public static void main(String[] args) {
SpringApplication.run(GeorchestraGatewayApplication.class, args);
}

@GetMapping(path = "/whoami", produces = "application/json")
@ResponseBody
public Mono<Principal> whoami(Principal principal) {
return principal == null ? Mono.empty() : Mono.just(principal);
public Mono<Map<String, Object>> whoami(Authentication principal, ServerWebExchange exchange) {
GeorchestraUser user = userMapper.resolve(principal).orElse(null);
Map<String, Object> ret = new LinkedHashMap<>();
ret.put("GeorchestraUser", user);
if (principal == null) {
ret.put("Authentication", null);
} else {
ret.put(principal.getClass().getCanonicalName(), principal);
}
return Mono.just(ret);
// return principal == null ? Mono.empty() :
// Mono.just(Map.of(principal.getClass().getCanonicalName(), principal));
}

@EventListener(ApplicationReadyEvent.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2021 by the geOrchestra PSC
*
* This file is part of geOrchestra.
*
* geOrchestra is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* geOrchestra is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* geOrchestra. If not, see <http://www.gnu.org/licenses/>.
*/
package org.georchestra.gateway.autoconfigure.app;

import org.georchestra.gateway.filter.global.ResolveTargetGlobalFilter;
import org.georchestra.gateway.filter.headers.HeaderFiltersConfiguration;
import org.georchestra.gateway.model.GatewayConfigProperties;
import org.georchestra.gateway.model.GeorchestraTargetConfig;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(GatewayAutoConfiguration.class)
@Import(HeaderFiltersConfiguration.class)
@EnableConfigurationProperties(GatewayConfigProperties.class)
public class FiltersAutoConfiguration {

/**
* {@link GlobalFilter} to {@link GeorchestraTargetConfig#setTarget save) the
* matched Route's GeorchestraTargetConfig for each HTTP request-response
* interaction before other filters are applied.
*/
public @Bean ResolveTargetGlobalFilter resolveTargetWebFilter(GatewayConfigProperties config) {
return new ResolveTargetGlobalFilter(config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@
package org.georchestra.gateway.autoconfigure.app;

import org.georchestra.gateway.handler.predicate.QueryParamRoutePredicateFactory;
import org.georchestra.gateway.model.GatewayConfigProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(GatewayConfigProperties.class)
@Configuration(proxyBeanMethods = false)
public class RoutePredicateFactoriesAutoConfiguration {

public @Bean QueryParamRoutePredicateFactory queryParamRoutePredicateFactory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(LdapTemplate.class)
@Import({ LdapSecurityAutoConfiguration.Enabled.class, LdapSecurityAutoConfiguration.Disabled.class })
@Slf4j(topic = "org.georchestra.gateway.autoconfigure.security.ldap")
@Slf4j(topic = "org.georchestra.gateway.autoconfigure.security")
public class LdapSecurityAutoConfiguration {

private static final String ENABLED_PROP = "georchestra.gateway.security.ldap.enabled";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import lombok.extern.slf4j.Slf4j;

@Configuration(proxyBeanMethods = false)
@Slf4j(topic = "org.georchestra.gateway.autoconfigure.security.ldap")
@Slf4j(topic = "org.georchestra.gateway.autoconfigure.security")
@Import({ OAuth2SecurityAutoConfiguration.Enabled.class, OAuth2SecurityAutoConfiguration.Disabled.class })
public class OAuth2SecurityAutoConfiguration {
private static final String ENABLED_PROP = "georchestra.gateway.security.oauth2.enabled";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2022 by the geOrchestra PSC
*
* This file is part of geOrchestra.
*
* geOrchestra is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* geOrchestra is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* geOrchestra. If not, see <http://www.gnu.org/licenses/>.
*/
package org.georchestra.gateway.filter.global;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

import java.net.URI;
import java.util.Objects;

import org.georchestra.gateway.model.GatewayConfigProperties;
import org.georchestra.gateway.model.GeorchestraTargetConfig;
import org.georchestra.gateway.model.Service;
import org.georchestra.gateway.security.ResolveGeorchestraUserGlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

/**
* A {@link GlobalFilter} that resolves the {@link GeorchestraTargetConfig
* configuration} for the request's matched {@link Route} and
* {@link GeorchestraTargetConfig#setTarget stores} it to be
* {@link GeorchestraTargetConfig#getTarget acquired} by non-global filters as
* needed.
*/
@RequiredArgsConstructor
@Slf4j
public class ResolveTargetGlobalFilter implements GlobalFilter, Ordered {

public static final int ORDER = ResolveGeorchestraUserGlobalFilter.ORDER + 1;

private final @NonNull GatewayConfigProperties config;

/**
* @return a lower precedence than {@link RouteToRequestUrlFilter}'s, in order
* to make sure the matched {@link Route} has been set as a
* {@link ServerWebExchange#getAttributes attribute} when
* {@link #filter} is called.
*/
public @Override int getOrder() {
return ResolveTargetGlobalFilter.ORDER;
}

/**
* Resolves the matched {@link Route} and its corresponding
* {@link GeorchestraTargetConfig}, if possible, and proceeds with the filter
* chain.
*/
public @Override Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = (Route) exchange.getAttributes().get(GATEWAY_ROUTE_ATTR);
if (null == route) {
log.info("Requested URI didn't match any Route, geOrchestra target resolution ignored.");
} else {
GeorchestraTargetConfig config = resolveTarget(route);
log.debug("Storing geOrchestra target config for Route {} request context", route.getId());
GeorchestraTargetConfig.setTarget(exchange, config);
}
return chain.filter(exchange);
}

private @NonNull GeorchestraTargetConfig resolveTarget(@NonNull Route route) {

GeorchestraTargetConfig target = new GeorchestraTargetConfig().headers(config.getDefaultHeaders())
.accessRules(config.getGlobalAccessRules());

final URI routeURI = route.getUri();

for (Service service : config.getServices().values()) {
var serviceURI = service.getTarget();
if (Objects.equals(routeURI, serviceURI)) {
if (!service.getAccessRules().isEmpty())
target.accessRules(service.getAccessRules());
if (service.getHeaders().isPresent())
target.headers(service.getHeaders().get());
break;
}
}
return target;
}

}
Loading

0 comments on commit fae09e7

Please sign in to comment.