From fae09e74fc30f1917bee983c6dfe7760921c005b Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Thu, 7 Apr 2022 17:29:00 -0300 Subject: [PATCH] Support appending sec-* headers as configured, resolved from authenticated user --- configuration.md | 72 +++++++++++ .../app/GeorchestraGatewayApplication.java | 22 +++- .../app/FiltersAutoConfiguration.java | 47 +++++++ ...tePredicateFactoriesAutoConfiguration.java | 5 +- .../LdapSecurityAutoConfiguration.java | 2 +- .../OAuth2SecurityAutoConfiguration.java | 2 +- .../global/ResolveTargetGlobalFilter.java | 104 +++++++++++++++ .../AddSecHeadersGatewayFilterFactory.java | 120 ++++-------------- .../filter/headers/HeaderContributor.java | 93 ++++++++++++++ .../headers/HeaderFiltersConfiguration.java} | 47 ++++--- ...veSecurityHeadersGatewayFilterFactory.java | 4 +- ...chestraOrganizationHeadersContributor.java | 42 ++++++ .../GeorchestraUserHeadersContributor.java | 61 +++++++++ .../SecProxyHeaderContributor.java} | 24 ++-- .../model/GatewayConfigProperties.java | 13 +- .../model/GeorchestraOrganization.java | 98 ++++++++++++++ .../model/GeorchestraTargetConfig.java | 50 ++++++++ .../gateway/model/GeorchestraUser.java | 108 ++++++++++++++++ .../gateway/model/HeaderMappings.java | 6 +- .../gateway/model/RoleBasedAccessRule.java | 29 ++++- .../georchestra/gateway/model/Service.java | 22 +++- .../GatewaySecurityConfiguration.java | 41 +++++- .../security/GeorchestraUserMapper.java | 44 +++++++ .../GeorchestraUserMapperExtension.java} | 19 ++- .../ResolveGeorchestraUserGlobalFilter.java | 82 ++++++++++++ .../ServerHttpSecurityCustomizer.java | 48 ++++++- .../accessrules/AccessRulesCustomizer.java | 14 ++ .../ldap/LdapAuthenticatedUserMapper.java | 61 +++++++++ .../ldap/LdapSecurityConfiguration.java | 7 +- ...h2AuthenticationTokenOpenIDUserMapper.java | 100 +++++++++++++++ .../OAuth2AuthenticationTokenUserMapper.java | 97 ++++++++++++++ .../security/oauth2/OAuth2Configuration.java | 17 ++- src/main/resources/META-INF/spring.factories | 2 +- src/main/resources/application.yml | 13 -- src/main/resources/gateway.yml | 44 +++---- src/main/resources/static/favicon.ico | Bin 0 -> 4262 bytes .../GeorchestraGatewayApplicationTests.java | 3 +- .../app/FiltersAutoConfigurationTest.java | 59 +++++++++ ...edicateFactoriesAutoConfigurationTest.java | 45 +++++++ .../OAuth2SecurityAutoConfigurationTest.java | 81 ++++++++++++ ...RemoveHeadersGatewayFilterFactoryTest.java | 6 +- ...curityHeadersGatewayFilterFactoryTest.java | 6 +- 42 files changed, 1550 insertions(+), 210 deletions(-) create mode 100644 configuration.md create mode 100644 src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java create mode 100644 src/main/java/org/georchestra/gateway/filter/global/ResolveTargetGlobalFilter.java create mode 100644 src/main/java/org/georchestra/gateway/filter/headers/HeaderContributor.java rename src/main/java/org/georchestra/gateway/{autoconfigure/app/HeaderFiltersAutoConfiguration.java => filter/headers/HeaderFiltersConfiguration.java} (56%) create mode 100644 src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraOrganizationHeadersContributor.java create mode 100644 src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java rename src/main/java/org/georchestra/gateway/filter/headers/{StandardSecurityHeadersProvider.java => providers/SecProxyHeaderContributor.java} (61%) create mode 100644 src/main/java/org/georchestra/gateway/model/GeorchestraOrganization.java create mode 100644 src/main/java/org/georchestra/gateway/model/GeorchestraTargetConfig.java create mode 100644 src/main/java/org/georchestra/gateway/model/GeorchestraUser.java create mode 100644 src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java rename src/main/java/org/georchestra/gateway/{filter/headers/HeaderProvider.java => security/GeorchestraUserMapperExtension.java} (62%) create mode 100644 src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java create mode 100644 src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticatedUserMapper.java create mode 100644 src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenOpenIDUserMapper.java create mode 100644 src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenUserMapper.java create mode 100644 src/main/resources/static/favicon.ico create mode 100644 src/test/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfigurationTest.java create mode 100644 src/test/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfigurationTest.java create mode 100644 src/test/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfigurationTest.java diff --git a/configuration.md b/configuration.md new file mode 100644 index 00000000..71c8d98e --- /dev/null +++ b/configuration.md @@ -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 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 +``` \ No newline at end of file diff --git a/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java b/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java index 43a2ae3d..f0ec354f 100644 --- a/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java +++ b/src/main/java/org/georchestra/gateway/app/GeorchestraGatewayApplication.java @@ -18,8 +18,11 @@ */ 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; @@ -27,10 +30,12 @@ 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; @@ -41,6 +46,7 @@ public class GeorchestraGatewayApplication { private @Autowired RouteLocator routeLocator; + private @Autowired GeorchestraUserMapper userMapper; public static void main(String[] args) { SpringApplication.run(GeorchestraGatewayApplication.class, args); @@ -48,8 +54,18 @@ public static void main(String[] args) { @GetMapping(path = "/whoami", produces = "application/json") @ResponseBody - public Mono whoami(Principal principal) { - return principal == null ? Mono.empty() : Mono.just(principal); + public Mono> whoami(Authentication principal, ServerWebExchange exchange) { + GeorchestraUser user = userMapper.resolve(principal).orElse(null); + Map 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) diff --git a/src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java b/src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java new file mode 100644 index 00000000..ba68a7bc --- /dev/null +++ b/src/main/java/org/georchestra/gateway/autoconfigure/app/FiltersAutoConfiguration.java @@ -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 . + */ +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); + } +} diff --git a/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java b/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java index dc11f435..18a70bf2 100644 --- a/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java +++ b/src/main/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfiguration.java @@ -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() { diff --git a/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java b/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java index a8dfa84e..1ecc8a86 100644 --- a/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java +++ b/src/main/java/org/georchestra/gateway/autoconfigure/security/LdapSecurityAutoConfiguration.java @@ -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"; diff --git a/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java b/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java index b2b45cea..8c625439 100644 --- a/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java +++ b/src/main/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfiguration.java @@ -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"; diff --git a/src/main/java/org/georchestra/gateway/filter/global/ResolveTargetGlobalFilter.java b/src/main/java/org/georchestra/gateway/filter/global/ResolveTargetGlobalFilter.java new file mode 100644 index 00000000..24bc9704 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/filter/global/ResolveTargetGlobalFilter.java @@ -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 . + */ +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 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; + } + +} \ No newline at end of file diff --git a/src/main/java/org/georchestra/gateway/filter/headers/AddSecHeadersGatewayFilterFactory.java b/src/main/java/org/georchestra/gateway/filter/headers/AddSecHeadersGatewayFilterFactory.java index c04cf427..e935c9dd 100644 --- a/src/main/java/org/georchestra/gateway/filter/headers/AddSecHeadersGatewayFilterFactory.java +++ b/src/main/java/org/georchestra/gateway/filter/headers/AddSecHeadersGatewayFilterFactory.java @@ -18,134 +18,60 @@ */ package org.georchestra.gateway.filter.headers; -import java.security.Principal; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.georchestra.gateway.model.GatewayConfigProperties; -import org.springframework.beans.factory.annotation.Autowired; +import org.georchestra.gateway.filter.global.ResolveTargetGlobalFilter; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.server.ServerWebExchange; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; public class AddSecHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory { - private @Autowired(required = false) List providers = new ArrayList<>(); - private @Autowired GatewayConfigProperties gatewayConfig; + private final List providers; - public AddSecHeadersGatewayFilterFactory() { + public AddSecHeadersGatewayFilterFactory(List providers) { super(NameConfig.class); + this.providers = providers; } - @Override - public List shortcutFieldOrder() { + public @Override List shortcutFieldOrder() { return Arrays.asList(NAME_KEY); } - @Override - public GatewayFilter apply(NameConfig config) { - return new AddSecHeadersGatewayFilter(); -// return (exchange, chain) -> { -// Mono auth = ReactiveSecurityContextHolder.getContext() -// .switchIfEmpty(Mono.error(new IllegalStateException("ReactiveSecurityContext is empty"))) -// .map(SecurityContext::getAuthentication); -// -// Mono principal = exchange.getPrincipal(); -// -// Mono name = auth.doOnNext(principal -> { -// System.err.println(principal); -// }).map(Principal::getName); -// -// Mono p = exchange.getPrincipal(); -// Mono name = p.doOnNext(principal -> { -// System.err.println(principal); -// }).map(Principal::getName); -// -// ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate(); -// providers.forEach(provider -> requestBuilder.headers(provider.prepare(exchange))); -// -// ServerHttpRequest request = requestBuilder// -// .header("sec-proxy", "true")// -// .header("sec-username", "testuser")// -// .header("sec-org", "PSC")// -// .header("sec-roles", "ROLE_USER")// -// .build(); -// -// return chain.filter(exchange.mutate().request(request).build()); -// }; + public @Override GatewayFilter apply(NameConfig config) { + return new AddSecHeadersGatewayFilter(providers); } - private static class AddSecHeadersGatewayFilter implements GatewayFilter { + @RequiredArgsConstructor + private static class AddSecHeadersGatewayFilter implements GatewayFilter, Ordered { - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - Mono name = exchange.getPrincipal().map(this::resolveName); - Mono e = name.map(n -> this.addHeaders(n, exchange)).defaultIfEmpty(exchange); - Mono flatMap = e.flatMap(chain::filter); - return flatMap; - } + private final @NonNull List providers; - private ServerWebExchange addHeaders(String name, ServerWebExchange exchange) { + public @Override Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate(); - // providers.forEach(provider -> - // requestBuilder.headers(provider.prepare(exchange))); - ServerHttpRequest request = requestBuilder// - .header("sec-proxy", "true")// - .header("sec-username", name)// - .header("sec-org", "PSC")// - .header("sec-roles", "ROLE_ADMINISTRATOR;ROLE_GNADMIN,ROLE_SUPERUSER")// - .build(); + providers.stream()// + .map(provider -> provider.prepare(exchange))// + .forEach(requestBuilder::headers); - return exchange.mutate().request(request).build(); + ServerHttpRequest request = requestBuilder.build(); + ServerWebExchange updatedExchange = exchange.mutate().request(request).build(); + return chain.filter(updatedExchange); } - private String resolveName(Principal p) { - System.err.println("Resolving " + p.getClass().getName()); - String name; - if (p instanceof OAuth2AuthenticationToken) { - OAuth2AuthenticationToken u = ((OAuth2AuthenticationToken) p); - Collection authorities = u.getAuthorities(); - String authorizedClientRegistrationId = u.getAuthorizedClientRegistrationId(); - Object credentials = u.getCredentials(); - Object details = u.getDetails(); - String n = u.getName(); - OAuth2User principal = u.getPrincipal(); - Map attributes = principal.getAttributes(); - String name2 = principal.getName(); - Map attributes2 = principal.getAttributes(); - name = (String) attributes2.get("email"); - } else { - name = p.getName(); - } - return name; + @Override + public int getOrder() { + return ResolveTargetGlobalFilter.ORDER + 1; } } -// return (exchange, chain) -> exchange.getPrincipal() -// // .log("token-relay-filter") -// .filter(principal -> principal instanceof OAuth2AuthenticationToken) -// .cast(OAuth2AuthenticationToken.class) -// .flatMap(authentication -> authorizedClient(exchange, authentication)) -// .map(OAuth2AuthorizedClient::getAccessToken).map(token -> withBearerAuth(exchange, token)) -// // TODO: adjustable behavior if empty -// .defaultIfEmpty(exchange).flatMap(chain::filter); - - private Optional extractService(ServerWebExchange exchange) { - // TODO Auto-generated method stub - return null; - } - } diff --git a/src/main/java/org/georchestra/gateway/filter/headers/HeaderContributor.java b/src/main/java/org/georchestra/gateway/filter/headers/HeaderContributor.java new file mode 100644 index 00000000..dd879d96 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/filter/headers/HeaderContributor.java @@ -0,0 +1,93 @@ +/* + * 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 . + */ +package org.georchestra.gateway.filter.headers; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.georchestra.gateway.filter.headers.providers.GeorchestraOrganizationHeadersContributor; +import org.georchestra.gateway.filter.headers.providers.GeorchestraUserHeadersContributor; +import org.georchestra.gateway.filter.headers.providers.SecProxyHeaderContributor; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.web.server.ServerWebExchange; + +import lombok.extern.slf4j.Slf4j; + +/** + * Extension point to aid {@link AddSecHeadersGatewayFilterFactory} in appending + * the required HTTP request headers to proxied requests. + *

+ * Beans of this type are strategy objects that contribute zero or more HTTP + * request headers to be appended to proxied requests to back-end services. + * + * @see SecProxyHeaderContributor + * @see GeorchestraUserHeadersContributor + * @see GeorchestraOrganizationHeadersContributor + */ +@Slf4j +public abstract class HeaderContributor implements Ordered { + + /** + * Prepare a header contributor for the given HTTP request-response interaction. + *

+ * The returned consumer will {@link HttpHeaders#set(String, String) set} or + * {@link HttpHeaders#add(String, String) add} whatever request headers are + * appropriate for the backend service. + */ + public abstract Consumer prepare(ServerWebExchange exchange); + + /** + * {@inheritDoc} + * + * @return {@code 0} as default order, implementations should override as needed + * in case they need to apply their customizations to + * {@link ServerHttpSecurity} in a specific order. + * @see Ordered#HIGHEST_PRECEDENCE + * @see Ordered#LOWEST_PRECEDENCE + */ + public @Override int getOrder() { + return 0; + } + + protected void add(HttpHeaders target, String header, Optional enabled, Optional value) { + add(target, header, enabled, value.orElse(null)); + } + + protected void add(HttpHeaders target, String header, Optional enabled, List values) { + add(target, header, enabled, values.stream().collect(Collectors.joining(";"))); + } + + protected void add(HttpHeaders target, String header, Optional enabled, String value) { + if (enabled.orElse(Boolean.FALSE).booleanValue()) { + if (null == value) { + log.debug("Value for header {} is not present", header); + } else { + log.info("Appending header {}: {}", header, value); + target.add(header, value); + } + } else { + log.debug("Header {} is not enabled", header); + } + } + +} diff --git a/src/main/java/org/georchestra/gateway/autoconfigure/app/HeaderFiltersAutoConfiguration.java b/src/main/java/org/georchestra/gateway/filter/headers/HeaderFiltersConfiguration.java similarity index 56% rename from src/main/java/org/georchestra/gateway/autoconfigure/app/HeaderFiltersAutoConfiguration.java rename to src/main/java/org/georchestra/gateway/filter/headers/HeaderFiltersConfiguration.java index 70663090..08ad4cf9 100644 --- a/src/main/java/org/georchestra/gateway/autoconfigure/app/HeaderFiltersAutoConfiguration.java +++ b/src/main/java/org/georchestra/gateway/filter/headers/HeaderFiltersConfiguration.java @@ -16,35 +16,46 @@ * You should have received a copy of the GNU General Public License along with * geOrchestra. If not, see . */ -package org.georchestra.gateway.autoconfigure.app; +package org.georchestra.gateway.filter.headers; -import org.georchestra.gateway.filter.headers.AddSecHeadersGatewayFilterFactory; -import org.georchestra.gateway.filter.headers.RemoveHeadersGatewayFilterFactory; -import org.georchestra.gateway.filter.headers.RemoveSecurityHeadersGatewayFilterFactory; -import org.georchestra.gateway.filter.headers.StandardSecurityHeadersProvider; -import org.georchestra.gateway.model.GatewayConfigProperties; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; +import java.util.List; + +import org.georchestra.gateway.filter.headers.providers.GeorchestraOrganizationHeadersContributor; +import org.georchestra.gateway.filter.headers.providers.GeorchestraUserHeadersContributor; +import org.georchestra.gateway.filter.headers.providers.SecProxyHeaderContributor; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration -@AutoConfigureBefore(GatewayAutoConfiguration.class) -@EnableConfigurationProperties(GatewayConfigProperties.class) -public class HeaderFiltersAutoConfiguration { +@Configuration(proxyBeanMethods = false) +public class HeaderFiltersConfiguration { /** * {@link GatewayFilterFactory} to add all necessary {@literal sec-*} request - * headers to proxied requests + * headers to proxied requests. + * + * @param providers the list of configured {@link HeaderContributor}s in the + * {@link ApplicationContext} + * @see #secProxyHeaderProvider() + * @see #userSecurityHeadersProvider() + * @see #organizationSecurityHeadersProvider() */ - public @Bean AddSecHeadersGatewayFilterFactory addSecHeadersGatewayFilterFactory() { - return new AddSecHeadersGatewayFilterFactory(); + public @Bean AddSecHeadersGatewayFilterFactory addSecHeadersGatewayFilterFactory( + List providers) { + return new AddSecHeadersGatewayFilterFactory(providers); + } + + public @Bean GeorchestraUserHeadersContributor userSecurityHeadersProvider() { + return new GeorchestraUserHeadersContributor(); + } + + public @Bean SecProxyHeaderContributor secProxyHeaderProvider() { + return new SecProxyHeaderContributor(); } - public @Bean StandardSecurityHeadersProvider standardSecurityHeadersProvider() { - return new StandardSecurityHeadersProvider(); + public @Bean GeorchestraOrganizationHeadersContributor organizationSecurityHeadersProvider() { + return new GeorchestraOrganizationHeadersContributor(); } /** diff --git a/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java b/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java index 3c928e8a..e94eca27 100644 --- a/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java +++ b/src/main/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactory.java @@ -24,7 +24,9 @@ /** * Georchestra-specific {@link GatewayFilterFactory} to remove all incoming - * {@code sec-*} and {@code Authorization} (basic auth) request headers. + * {@code sec-*} and {@code Authorization} (basic auth) request headers, hence + * preventing impersonating geOrchestra authenticated users from incoming + * requests. *

* Sample usage: * diff --git a/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraOrganizationHeadersContributor.java b/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraOrganizationHeadersContributor.java new file mode 100644 index 00000000..3558670e --- /dev/null +++ b/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraOrganizationHeadersContributor.java @@ -0,0 +1,42 @@ +/* + * 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 . + */ +package org.georchestra.gateway.filter.headers.providers; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.georchestra.gateway.filter.headers.HeaderContributor; +import org.georchestra.gateway.model.GeorchestraOrganization; +import org.georchestra.gateway.model.GeorchestraTargetConfig; +import org.springframework.http.HttpHeaders; +import org.springframework.web.server.ServerWebExchange; + +public class GeorchestraOrganizationHeadersContributor extends HeaderContributor { + + public @Override Consumer prepare(ServerWebExchange exchange) { + return headers -> { + GeorchestraTargetConfig.getTarget(exchange)// + .map(GeorchestraTargetConfig::headers)// + .ifPresent(mappings -> { + Optional org = GeorchestraOrganization.resolve(exchange); + add(headers, "sec-orgname", mappings.getOrgname(), org.map(GeorchestraOrganization::getName)); + }); + }; + } +} diff --git a/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java b/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java new file mode 100644 index 00000000..ae1a7b54 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java @@ -0,0 +1,61 @@ +/* + * 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 . + */ +package org.georchestra.gateway.filter.headers.providers; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import org.georchestra.gateway.filter.headers.HeaderContributor; +import org.georchestra.gateway.model.GeorchestraTargetConfig; +import org.georchestra.gateway.model.GeorchestraUser; +import org.springframework.http.HttpHeaders; +import org.springframework.web.server.ServerWebExchange; + +/** + * Contributes user-related {@literal sec-*} request headers. + * + *

+ * For any + * + * @see GeorchestraUser + * @see GeorchestraTargetConfig + */ +public class GeorchestraUserHeadersContributor extends HeaderContributor { + + public @Override Consumer prepare(ServerWebExchange exchange) { + return headers -> { + GeorchestraTargetConfig.getTarget(exchange)// + .map(GeorchestraTargetConfig::headers)// + .ifPresent(mappings -> { + Optional user = GeorchestraUser.resolve(exchange); + add(headers, "sec-proxy", mappings.getProxy(), "true"); + add(headers, "sec-username", mappings.getUsername(), user.map(GeorchestraUser::getUsername)); + add(headers, "sec-org", mappings.getOrg(), user.map(GeorchestraUser::getOrganization)); + add(headers, "sec-email", mappings.getEmail(), user.map(GeorchestraUser::getEmail)); + add(headers, "sec-firstname", mappings.getFirstname(), user.map(GeorchestraUser::getFirstName)); + add(headers, "sec-lastname", mappings.getLastname(), user.map(GeorchestraUser::getLastName)); + add(headers, "sec-tel", mappings.getTel(), user.map(GeorchestraUser::getTelephoneNumber)); + + List roles = user.map(GeorchestraUser::getRoles).orElse(List.of()); + add(headers, "sec-roles", mappings.getRoles(), roles); + }); + }; + } +} diff --git a/src/main/java/org/georchestra/gateway/filter/headers/StandardSecurityHeadersProvider.java b/src/main/java/org/georchestra/gateway/filter/headers/providers/SecProxyHeaderContributor.java similarity index 61% rename from src/main/java/org/georchestra/gateway/filter/headers/StandardSecurityHeadersProvider.java rename to src/main/java/org/georchestra/gateway/filter/headers/providers/SecProxyHeaderContributor.java index 912e0fe2..6a377c00 100644 --- a/src/main/java/org/georchestra/gateway/filter/headers/StandardSecurityHeadersProvider.java +++ b/src/main/java/org/georchestra/gateway/filter/headers/providers/SecProxyHeaderContributor.java @@ -16,28 +16,24 @@ * You should have received a copy of the GNU General Public License along with * geOrchestra. If not, see . */ -package org.georchestra.gateway.filter.headers; +package org.georchestra.gateway.filter.headers.providers; -import java.net.URI; import java.util.function.Consumer; -import org.georchestra.gateway.model.GatewayConfigProperties; -import org.springframework.beans.factory.annotation.Autowired; +import org.georchestra.gateway.filter.headers.HeaderContributor; import org.springframework.http.HttpHeaders; import org.springframework.web.server.ServerWebExchange; -public class StandardSecurityHeadersProvider implements HeaderProvider { - - private @Autowired GatewayConfigProperties config; +/** + * Unconditionally contributes the {@literal sec-proxy: true} request header, + * which is required by all backend services as a flag indicating the request is + * authenticated. + */ +public class SecProxyHeaderContributor extends HeaderContributor { - @Override - public Consumer prepare(ServerWebExchange exchange) { + public @Override Consumer prepare(ServerWebExchange exchange) { return headers -> { - URI uri = exchange.getRequest().getURI(); - String path = uri.getPath(); - GatewayConfigProperties c = config; - System.err.println(uri); + headers.add("sec-proxy", "true"); }; } - } diff --git a/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java b/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java index 86775a22..202f22a5 100644 --- a/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java +++ b/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java @@ -26,6 +26,12 @@ import lombok.Data; +/** + * Model object representing the externalized configuration properties used to + * set up URI based access rules and HTTP request headers appended to proxied + * requests to back-end services. + * + */ @Data @ConfigurationProperties("georchestra.gateway") public class GatewayConfigProperties { @@ -33,18 +39,17 @@ public class GatewayConfigProperties { /** * Configures the global security headers to append to all proxied http requests */ - private HeaderMappings defaultHeaders = new HeaderMappings(); + private HeaderMappings defaultHeaders; /** * Incoming request URI pattern matching for requests that don't match any of * the service-specific rules under - * georchestra.gateway.services.[service].access-rules + * {@literal georchestra.gateway.services.[service].access-rules} */ private List globalAccessRules; /** - * Maps a logical service name, to its back-end service URL and security - * settings + * Maps a logical service name to its back-end service URL and security settings */ private Map services = Collections.emptyMap(); diff --git a/src/main/java/org/georchestra/gateway/model/GeorchestraOrganization.java b/src/main/java/org/georchestra/gateway/model/GeorchestraOrganization.java new file mode 100644 index 00000000..4b92ff4c --- /dev/null +++ b/src/main/java/org/georchestra/gateway/model/GeorchestraOrganization.java @@ -0,0 +1,98 @@ +/* + * 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 . + */ +package org.georchestra.gateway.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.springframework.web.server.ServerWebExchange; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.With; + +@Data +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@With +public class GeorchestraOrganization implements Serializable { + + static final String GEORCHESTRA_ORGANIZATION_KEY = GeorchestraOrganization.class.getCanonicalName(); + + private static final long serialVersionUID = -1; + + /** Provided by request header {@code sec-orgid}, usually stable UUID */ + private String id; + + /** + * Provided by request header {@code sec-org}, legacy way of identifying by + * LDAP's {@code org.cn} attribute, which may change over time + */ + private String shortName; + + /** + * Provided by request header {@code sec-orgname}, due to legacy LDAP mapping + * {@code sec-orgname=org.o} + */ + private String name; + + /** Provided by request header {@code sec-org-linkage} */ + private String linkage; + + /** Provided by request header {@code sec-org-address} */ + private String postalAddress; + + /** Provided by request header {@code sec-org-category} */ + private String category; + + /** Provided by request header {@code sec-org-description} */ + private String description; + + /** Provided by request header {@code sec-org-notes} */ + private String notes; + + /** + * String that somehow represents the current version, may be a timestamp, a + * hash, etc. Provided by request header {@code sec-lastupdated} + */ + private String lastUpdated; + + /** + * List of {@link GeorchestraUser#getUsername() user names} that belong to this + * organization + */ + private List members = new ArrayList<>(); + + public void setMembers(List members) { + this.members = members == null ? new ArrayList<>() : members; + } + + public static Optional resolve(ServerWebExchange exchange) { + return Optional.ofNullable(exchange.getAttribute(GEORCHESTRA_ORGANIZATION_KEY)) + .map(GeorchestraOrganization.class::cast); + } + + public static void store(ServerWebExchange exchange, GeorchestraOrganization org) { + exchange.getAttributes().put(GEORCHESTRA_ORGANIZATION_KEY, org); + } +} \ No newline at end of file diff --git a/src/main/java/org/georchestra/gateway/model/GeorchestraTargetConfig.java b/src/main/java/org/georchestra/gateway/model/GeorchestraTargetConfig.java new file mode 100644 index 00000000..96458438 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/model/GeorchestraTargetConfig.java @@ -0,0 +1,50 @@ +/* + * 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 . + */ +package org.georchestra.gateway.model; + +import java.util.List; +import java.util.Optional; + +import org.springframework.cloud.gateway.route.Route; +import org.springframework.web.server.ServerWebExchange; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * The HTTP request headers and role-based access rules of a matched + * {@link Route} + */ +@Data +@Accessors(fluent = true, chain = true) +public class GeorchestraTargetConfig { + + static final String TARGET_CONFIG_KEY = GeorchestraTargetConfig.class.getCanonicalName() + ".target"; + + private HeaderMappings headers; + private List accessRules; + + public static Optional getTarget(ServerWebExchange exchange) { + return Optional.ofNullable(exchange.getAttribute(TARGET_CONFIG_KEY)).map(GeorchestraTargetConfig.class::cast); + } + + public static void setTarget(ServerWebExchange exchange, GeorchestraTargetConfig config) { + exchange.getAttributes().put(TARGET_CONFIG_KEY, config); + } +} diff --git a/src/main/java/org/georchestra/gateway/model/GeorchestraUser.java b/src/main/java/org/georchestra/gateway/model/GeorchestraUser.java new file mode 100644 index 00000000..1b294284 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/model/GeorchestraUser.java @@ -0,0 +1,108 @@ +/* + * 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 . + */ +package org.georchestra.gateway.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.web.server.ServerWebExchange; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@NoArgsConstructor +public class GeorchestraUser implements Serializable { + + private static final long serialVersionUID = -1; + + static final String GEORCHESTRA_USER_KEY = GeorchestraUser.class.getCanonicalName(); + + /////// Default mandatory properties. ///// + /////// Some optional properties may be made mandatory on a per-application + /////// basis ///// + + /** Provided by request header {@code sec-username} */ + private String username; + + /** Provided by request header {@code sec-roles} */ + private List roles = new ArrayList<>(); + + /** + * User's organization short name. Provided by request header {@code sec-org}, + * legacy way of identifying by LDAP's {@code org.cn} attribute, which may + * change over time + */ + private String organization; + + /////// Default optional properties. ///// + /////// Some may be made mandatory on a per-application basis ///// + + /** Provided by request header {@code sec-userid} */ + private String id; + + /** + * String that somehow represents the current version, may be a timestamp, a + * hash, etc. Provided by request header {@code sec-lastupdated} + */ + private String lastUpdated; + + /** Provided by request header {@code sec-firstname} */ + private String firstName; + + /** Provided by request header {@code sec-lastname} */ + private String lastName; + + /** Provided by request header {@code sec-email} */ + private String email; + + /** Provided by request header {@code sec-address} */ + private String postalAddress; + + /** Provided by request header {@code sec-tel} */ + private String telephoneNumber; + + /** Provided by request header {@code sec-title} */ + private String title; + + /** Provided by request header {@code sec-notes} */ + private String notes; + + public void setRoles(List roles) { + this.roles = roles == null ? new ArrayList<>() : roles; + } + + public static Optional resolve(ServerWebExchange exchange) { + return Optional.ofNullable(exchange.getAttribute(GEORCHESTRA_USER_KEY)).map(GeorchestraUser.class::cast); + } + + public static ServerWebExchange store(@NonNull ServerWebExchange exchange, GeorchestraUser user) { + Map attributes = exchange.getAttributes(); + if (user == null) { + attributes.remove(GEORCHESTRA_USER_KEY); + } else { + attributes.put(GEORCHESTRA_USER_KEY, user); + } + return exchange; + } +} diff --git a/src/main/java/org/georchestra/gateway/model/HeaderMappings.java b/src/main/java/org/georchestra/gateway/model/HeaderMappings.java index 47d87baf..4b607431 100644 --- a/src/main/java/org/georchestra/gateway/model/HeaderMappings.java +++ b/src/main/java/org/georchestra/gateway/model/HeaderMappings.java @@ -22,6 +22,10 @@ import lombok.Data; +/** + * Models which geOrchestra-specific HTTP request headers to append to proxied + * requests. + */ @Data public class HeaderMappings { /** Append the standard {@literal sec-proxy=true} header to proxied requests */ @@ -45,7 +49,7 @@ public class HeaderMappings { /** Append the standard {@literal sec-firstname} header to proxied requests */ private Optional firstname = Optional.empty(); - /** Append the standard {@literal sec-firstname} header to proxied requests */ + /** Append the standard {@literal sec-lastname} header to proxied requests */ private Optional lastname = Optional.empty(); /** Append the standard {@literal sec-tel} header to proxied requests */ diff --git a/src/main/java/org/georchestra/gateway/model/RoleBasedAccessRule.java b/src/main/java/org/georchestra/gateway/model/RoleBasedAccessRule.java index 6c993193..bc74ef61 100644 --- a/src/main/java/org/georchestra/gateway/model/RoleBasedAccessRule.java +++ b/src/main/java/org/georchestra/gateway/model/RoleBasedAccessRule.java @@ -20,12 +20,37 @@ import java.util.List; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + import lombok.Data; +/** + * Models access rules to intercepted Ant-pattern URIs based on roles. + *

+ * Role names are defined by the authenticated user's + * {@link AbstractAuthenticationToken#getAuthorities() authority names} (i.e. + * {@link GrantedAuthority#getAuthority()}) . + */ @Data public class RoleBasedAccessRule { - private List interceptUrl; - private boolean anonymous; + /** + * List of Ant pattern URI's, excluding the application context, the Gateway + * shall intercept and apply the access rules defined here. E.g. + */ + private List interceptUrl = List.of(); + + /** + * Whether anonymous (unauthenticated) access is to be granted to the + * intercepted URIs. + */ + private boolean anonymous = false; + + /** + * Role names that the authenticated user must be part of to be granted access + * to the intercepted URIs. The ROLE_ prefix is optional. For example, the role + * set [ROLE_USER, ROLE_AUDITOR] is equivalent to [USER, AUDITOR] + */ private List allowedRoles = List.of(); } diff --git a/src/main/java/org/georchestra/gateway/model/Service.java b/src/main/java/org/georchestra/gateway/model/Service.java index db2f6019..47b7425e 100644 --- a/src/main/java/org/georchestra/gateway/model/Service.java +++ b/src/main/java/org/georchestra/gateway/model/Service.java @@ -18,22 +18,34 @@ */ package org.georchestra.gateway.model; -import java.net.URL; +import java.net.URI; import java.util.List; +import java.util.Optional; import lombok.Data; +/** + * Model object used to configure which authenticated user's roles can reach a + * given backend service URIs, and which HTTP request headers to append to the + * proxied requests. + * + */ @Data public class Service { /** - * Back end service URL + * Back end service URL the Gateway will use to proxy incoming requests to, + * based on the {@link #getAccessRules() access rules} + * {@link RoleBasedAccessRule#getInterceptUrl() intercept-URLs} */ - private URL target; + private URI target; /** * Service-specific security headers configuration */ - private HeaderMappings headers; + private Optional headers = Optional.empty(); - private List accessRules; + /** + * List of Ant-pattern based access rules for the given back-end service + */ + private List accessRules = List.of(); } diff --git a/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java b/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java index 5700fdce..9c878602 100644 --- a/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java +++ b/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java @@ -19,6 +19,7 @@ package org.georchestra.gateway.security; import java.util.List; +import java.util.stream.Stream; import org.georchestra.gateway.model.GatewayConfigProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -30,25 +31,46 @@ import lombok.extern.slf4j.Slf4j; +/** + * {@link Configuration} to initialize the Gateway's + * {@link SecurityWebFilterChain} during application start up, such as + * establishing path based access rules, configuring authentication providers, + * etc. + *

+ * Note this configuration does very little by itself. Instead, it relies on + * available beans implementing the {@link ServerHttpSecurityCustomizer} + * extension point to tweak the {@link ServerHttpSecurity} as appropriate in a + * decoupled way. + * + * @see ServerHttpSecurityCustomizer + */ @Configuration(proxyBeanMethods = false) @EnableWebFluxSecurity @EnableConfigurationProperties(GatewayConfigProperties.class) @Slf4j(topic = "org.georchestra.gateway.security") public class GatewaySecurityConfiguration { + /** + * Relies on available {@link ServerHttpSecurityCustomizer} extensions to + * configure the different aspects of the {@link ServerHttpSecurity} used to + * {@link ServerHttpSecurity#build build} the {@link SecurityWebFilterChain}. + */ @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, GatewayConfigProperties config, + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, List customizers) throws Exception { + log.info("Initializing security filter chain..."); // disable csrf and cors or the websocket connection gets a 403 Forbidden. // Revisit. + log.info("CSRF and CORS disabled. Revisit how they interfer with Websockets proxying."); http.csrf().disable().cors().disable(); - customizers.forEach(customizer -> { - log.debug("Applying security customizer " + customizer.getName()); + sortedCustomizers(customizers).forEach(customizer -> { + log.debug("Applying security customizer {}", customizer.getName()); customizer.customize(http); }); + log.info("Security filter chain initialized"); // http.authorizeExchange()// // .pathMatchers("/", "/header/**").permitAll()// // .pathMatchers("/ws/**").permitAll()// @@ -56,4 +78,17 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, Ga return http.build(); } + + private Stream sortedCustomizers(List customizers) { + return customizers.stream().sorted((c1, c2) -> Integer.compare(c1.getOrder(), c2.getOrder())); + } + + public @Bean GeorchestraUserMapper georchestraUserResolver(List resolvers) { + return new GeorchestraUserMapper(resolvers); + } + + public @Bean ResolveGeorchestraUserGlobalFilter resolveGeorchestraUserGlobalFilter(GeorchestraUserMapper resolver) { + return new ResolveGeorchestraUserGlobalFilter(resolver); + } + } diff --git a/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java b/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java new file mode 100644 index 00000000..d7ca5f32 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java @@ -0,0 +1,44 @@ +/* + * 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 . + */ +package org.georchestra.gateway.security; + +import java.util.List; +import java.util.Optional; + +import org.georchestra.gateway.model.GeorchestraUser; +import org.springframework.security.core.Authentication; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Relies on the provided {@link GeorchestraUserMapperExtension}s to map an + * {@link Authentication} to a {@link GeorchestraUser}. + */ +@RequiredArgsConstructor +public class GeorchestraUserMapper { + + private final @NonNull List resolvers; + + public Optional resolve(Authentication authToken) { + return resolvers.stream().map(resolver -> resolver.resolve(authToken)).filter(Optional::isPresent) + .map(Optional::get).findFirst(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/georchestra/gateway/filter/headers/HeaderProvider.java b/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapperExtension.java similarity index 62% rename from src/main/java/org/georchestra/gateway/filter/headers/HeaderProvider.java rename to src/main/java/org/georchestra/gateway/security/GeorchestraUserMapperExtension.java index 4b647564..323a6973 100644 --- a/src/main/java/org/georchestra/gateway/filter/headers/HeaderProvider.java +++ b/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapperExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 by the geOrchestra PSC + * Copyright (C) 2022 by the geOrchestra PSC * * This file is part of geOrchestra. * @@ -16,19 +16,18 @@ * You should have received a copy of the GNU General Public License along with * geOrchestra. If not, see . */ -package org.georchestra.gateway.filter.headers; -import java.util.function.Consumer; +package org.georchestra.gateway.security; -import org.springframework.http.HttpHeaders; -import org.springframework.web.server.ServerWebExchange; +import java.util.Optional; + +import org.georchestra.gateway.model.GeorchestraUser; +import org.springframework.security.core.Authentication; /** - * Extension point for contributors to HTTP request headers sent to back-end - * services. + * */ -public interface HeaderProvider { - - Consumer prepare(ServerWebExchange exchange); +public interface GeorchestraUserMapperExtension { + Optional resolve(Authentication authToken); } diff --git a/src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java b/src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java new file mode 100644 index 00000000..5bfe17e0 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/security/ResolveGeorchestraUserGlobalFilter.java @@ -0,0 +1,82 @@ +/* + * 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 . + */ +package org.georchestra.gateway.security; + +import java.util.Set; + +import org.georchestra.gateway.filter.global.ResolveTargetGlobalFilter; +import org.georchestra.gateway.model.GeorchestraTargetConfig; +import org.georchestra.gateway.model.GeorchestraUser; +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.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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 GeorchestraUser} from the + * request's {@link Authentication} so it can be {@link GeorchestraUser#resolve + * retrieved} down the road during the filter chain. + *

+ */ +@RequiredArgsConstructor +@Slf4j +public class ResolveGeorchestraUserGlobalFilter implements GlobalFilter, Ordered { + + public static final int ORDER = RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 1; + + private static final Authentication NONE = new AnonymousAuthenticationToken("none", new Object(), + Set.of(new SimpleGrantedAuthority("UNAUTHENTICATED"))); + + private final @NonNull GeorchestraUserMapper resolver; + + /** + * @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 filter(ServerWebExchange exchange, GatewayFilterChain chain) { + + return exchange.getPrincipal().doOnNext(p -> log.debug("resolving user from {}", p.getClass().getName())) + .filter(Authentication.class::isInstance).map(Authentication.class::cast).defaultIfEmpty(NONE) + .map(resolver::resolve).map(user -> { + return GeorchestraUser.store(exchange, user.orElse(null)); + }).flatMap(chain::filter); + } + +} \ No newline at end of file diff --git a/src/main/java/org/georchestra/gateway/security/ServerHttpSecurityCustomizer.java b/src/main/java/org/georchestra/gateway/security/ServerHttpSecurityCustomizer.java index 95805386..34ca7f52 100644 --- a/src/main/java/org/georchestra/gateway/security/ServerHttpSecurityCustomizer.java +++ b/src/main/java/org/georchestra/gateway/security/ServerHttpSecurityCustomizer.java @@ -1,11 +1,57 @@ +/* + * 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 . + */ package org.georchestra.gateway.security; +import org.springframework.core.Ordered; import org.springframework.security.config.Customizer; import org.springframework.security.config.web.server.ServerHttpSecurity; -public interface ServerHttpSecurityCustomizer extends Customizer { +/** + * Extension point to aid {@link GatewaySecurityConfiguration} in initializing + * the application security filter chain. + *

+ * Spring beans of this type implement {@link Ordered}, and will be called in + * sequence adhering to each bean's defined order. + *

+ * This interface extends {@link Customizer Customizer}. The + * {@link Customizer#customize customize(ServerHttpSecurity)} shall modify the + * provided server HTTP security configuration bean in whatever way needed. + */ +public interface ServerHttpSecurityCustomizer extends Customizer, Ordered { + /** + * @return user friendly extension name for logging purposes + */ default String getName() { return getClass().getCanonicalName(); } + + /** + * {@inheritDoc} + * + * @return {@code 0} as default order, implementations should override as needed + * in case they need to apply their customizations to + * {@link ServerHttpSecurity} in a specific order. + * @see Ordered#HIGHEST_PRECEDENCE + * @see Ordered#LOWEST_PRECEDENCE + */ + default @Override int getOrder() { + return 0; + } } diff --git a/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java b/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java index 531a355a..896114af 100644 --- a/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java +++ b/src/main/java/org/georchestra/gateway/security/accessrules/AccessRulesCustomizer.java @@ -24,6 +24,7 @@ import org.georchestra.gateway.model.GatewayConfigProperties; import org.georchestra.gateway.model.RoleBasedAccessRule; +import org.georchestra.gateway.model.Service; import org.georchestra.gateway.security.ServerHttpSecurityCustomizer; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec; @@ -33,6 +34,19 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +/** + * {@link ServerHttpSecurityCustomizer} to apply {@link RoleBasedAccessRule ROLE + * based access rules} at startup. + *

+ * The access rules are configured as + * {@link GatewayConfigProperties#getGlobalAccessRules() global rules}, and + * overridden if needed on a per-service basis from + * {@link GatewayConfigProperties#getServices()}. + * + * @see RoleBasedAccessRule + * @see GatewayConfigProperties#getGlobalAccessRules() + * @see Service#getAccessRules() + */ @RequiredArgsConstructor @Slf4j(topic = "org.georchestra.gateway.config.security.accessrules") public class AccessRulesCustomizer implements ServerHttpSecurityCustomizer { diff --git a/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticatedUserMapper.java b/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticatedUserMapper.java new file mode 100644 index 00000000..32fb50a8 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/security/ldap/LdapAuthenticatedUserMapper.java @@ -0,0 +1,61 @@ +/* + * 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 . + */ + +package org.georchestra.gateway.security.ldap; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.georchestra.gateway.model.GeorchestraUser; +import org.georchestra.gateway.security.GeorchestraUserMapperExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +/** + * @author groldan + * + */ +public class LdapAuthenticatedUserMapper implements GeorchestraUserMapperExtension { + + @Override + public Optional resolve(Authentication authToken) { + return Optional.ofNullable(authToken)// + .filter(UsernamePasswordAuthenticationToken.class::isInstance) + .map(UsernamePasswordAuthenticationToken.class::cast)// + .flatMap(this::map); + } + + Optional map(UsernamePasswordAuthenticationToken token) { + GeorchestraUser user = new GeorchestraUser(); + + Collection authorities = token.getAuthorities(); + List roles = authorities.stream().map(GrantedAuthority::getAuthority).toList(); + String name = token.getName(); + + Object principal = token.getPrincipal(); + Object details = token.getDetails(); + + user.setUsername(name); + user.setRoles(roles); + return Optional.of(user); + } + +} diff --git a/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java b/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java index 83feb999..27db29cc 100644 --- a/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java +++ b/src/main/java/org/georchestra/gateway/security/ldap/LdapSecurityConfiguration.java @@ -55,10 +55,15 @@ private final class LDAPAuthenticationCustomizer implements ServerHttpSecurityCu } @Bean - ServerHttpSecurityCustomizer ldapHttpBasicLoginFormEnabler() { + ServerHttpSecurityCustomizer ldapHttpBasicLoginFormEnablerExtension() { return new LDAPAuthenticationCustomizer(); } + @Bean + LdapAuthenticatedUserMapper ldapAuthenticatedUserMapperExtension() { + return new LdapAuthenticatedUserMapper(); + } + @Bean BaseLdapPathContextSource contextSource(LdapConfigProperties config) { LdapContextSource context = new LdapContextSource(); diff --git a/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenOpenIDUserMapper.java b/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenOpenIDUserMapper.java new file mode 100644 index 00000000..088d2d76 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenOpenIDUserMapper.java @@ -0,0 +1,100 @@ +/* + * 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 . + */ + +package org.georchestra.gateway.security.oauth2; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import org.georchestra.gateway.model.GeorchestraUser; +import org.slf4j.Logger; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.oidc.AddressStandardClaim; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.StandardClaimAccessor; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author groldan + * + */ +@Slf4j +public class OAuth2AuthenticationTokenOpenIDUserMapper extends OAuth2AuthenticationTokenUserMapper { + + @Override + public Optional resolve(Authentication authToken) { + return Optional.ofNullable(authToken)// + .filter(OAuth2AuthenticationToken.class::isInstance)// + .map(OAuth2AuthenticationToken.class::cast)// + .filter(token -> token.getPrincipal() instanceof OidcUser)// + .flatMap(this::map); + } + + protected @Override Optional map(OAuth2AuthenticationToken token) { + GeorchestraUser user = super.map(token).orElseGet(GeorchestraUser::new); + + OidcUser oidcUser = (OidcUser) token.getPrincipal(); + + apply((StandardClaimAccessor) oidcUser, user); + + // OAuth2 non-standardized attributes + Map attributes = oidcUser.getAttributes(); + // OpenId Connect merged claims from OidcUserInfo and OidcIdToken + Map claims = oidcUser.getClaims(); + OidcUserInfo userInfo = oidcUser.getUserInfo(); + OidcIdToken idToken = oidcUser.getIdToken(); + + return Optional.of(user); + } + + private void apply(StandardClaimAccessor standardClaims, GeorchestraUser target) { + AddressStandardClaim address = standardClaims.getAddress(); + String preferredUsername = standardClaims.getPreferredUsername(); + String givenName = standardClaims.getGivenName(); + String familyName = standardClaims.getFamilyName(); + + String fullName = standardClaims.getFullName(); + String email = standardClaims.getEmail(); + String phoneNumber = standardClaims.getPhoneNumber(); + + apply(target::setUsername, preferredUsername, email); + apply(target::setFirstName, givenName); + apply(target::setLastName, familyName); + apply(target::setEmail, email); + apply(target::setTelephoneNumber, phoneNumber); + } + + protected void apply(Consumer setter, String... candidates) { + for (String candidateValue : candidates) { + if (null != candidateValue) { + setter.accept(candidateValue); + break; + } + } + } + + protected @Override Logger logger() { + return log; + } +} diff --git a/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenUserMapper.java b/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenUserMapper.java new file mode 100644 index 00000000..e2d00581 --- /dev/null +++ b/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2AuthenticationTokenUserMapper.java @@ -0,0 +1,97 @@ +/* + * 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 . + */ + +package org.georchestra.gateway.security.oauth2; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import org.georchestra.gateway.model.GeorchestraUser; +import org.georchestra.gateway.security.GeorchestraUserMapperExtension; +import org.slf4j.Logger; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author groldan + * + */ +@Slf4j +public class OAuth2AuthenticationTokenUserMapper implements GeorchestraUserMapperExtension { + + @Override + public Optional resolve(Authentication authToken) { + return Optional.ofNullable(authToken)// + .filter(OAuth2AuthenticationToken.class::isInstance)// + .map(OAuth2AuthenticationToken.class::cast)// + .filter(token -> !(token.getPrincipal() instanceof OidcUser))// + .flatMap(this::map); + } + + protected Optional map(OAuth2AuthenticationToken token) { + logger().debug("Mapping {} authentication token from provider {}", + token.getPrincipal().getClass().getSimpleName(), token.getAuthorizedClientRegistrationId()); + + OAuth2User oAuth2User = token.getPrincipal(); + GeorchestraUser user = new GeorchestraUser(); + + List roles = resolveRoles(oAuth2User.getAuthorities()); + String userName = token.getName(); + + Map attributes = oAuth2User.getAttributes(); + String login = (String) attributes.get("login"); + apply(user::setUsername, login, userName); + apply(user::setEmail, (String) attributes.get("email")); + user.setRoles(roles); + + return Optional.of(user); + } + + protected List resolveRoles(Collection authorities) { + List roles = authorities.stream().map(GrantedAuthority::getAuthority).filter(scope -> { + if (scope.startsWith("ROLE_SCOPE_") || scope.startsWith("SCOPE_")) { + logger().debug("Excluding granted authority {}", scope); + return false; + } + return true; + }).toList(); + return roles; + } + + protected void apply(Consumer setter, String... candidates) { + for (String candidateValue : candidates) { + if (null != candidateValue) { + setter.accept(candidateValue); + break; + } + } + } + + protected Logger logger() { + return log; + } +} diff --git a/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java b/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java index aa1b8c50..0cecb452 100644 --- a/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java +++ b/src/main/java/org/georchestra/gateway/security/oauth2/OAuth2Configuration.java @@ -25,7 +25,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.security.authentication.ReactiveAuthenticationManager; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2LoginSpec; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; @@ -35,7 +34,6 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.web.reactive.function.client.WebClient; import lombok.extern.slf4j.Slf4j; @@ -43,12 +41,11 @@ import reactor.netty.transport.ProxyProvider; @Configuration(proxyBeanMethods = false) -@EnableWebFluxSecurity @EnableConfigurationProperties(OAuth2ProxyConfigProperties.class) @Slf4j(topic = "org.georchestra.gateway.security.oauth2") public class OAuth2Configuration { - private final class OAuth2AuthenticationCustomizer implements ServerHttpSecurityCustomizer { + public static final class OAuth2AuthenticationCustomizer implements ServerHttpSecurityCustomizer { public @Override void customize(ServerHttpSecurity http) { log.info("Enabling authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider"); @@ -57,10 +54,20 @@ private final class OAuth2AuthenticationCustomizer implements ServerHttpSecurity } @Bean - ServerHttpSecurityCustomizer oau2EnablingCustomizer() { + ServerHttpSecurityCustomizer oauth2LoginEnablingCustomizer() { return new OAuth2AuthenticationCustomizer(); } + @Bean + OAuth2AuthenticationTokenUserMapper oAuth2AuthenticationTokenUserMapper() { + return new OAuth2AuthenticationTokenUserMapper(); + } + + @Bean + OAuth2AuthenticationTokenOpenIDUserMapper oAuth2AuthenticationTokenOpenIDUserMapper() { + return new OAuth2AuthenticationTokenOpenIDUserMapper(); + } + /** * Configures the OAuth2 client to use the HTTP proxy if enabled, by means of * {@linkplain #oauth2WebClient} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index ee05d591..95c56c86 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -3,5 +3,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.georchestra.gateway.autoconfigure.security.WebSecurityAutoConfiguration,\ org.georchestra.gateway.autoconfigure.security.LdapSecurityAutoConfiguration,\ org.georchestra.gateway.autoconfigure.security.OAuth2SecurityAutoConfiguration,\ -org.georchestra.gateway.autoconfigure.app.HeaderFiltersAutoConfiguration,\ +org.georchestra.gateway.autoconfigure.app.FiltersAutoConfiguration,\ org.georchestra.gateway.autoconfigure.app.RoutePredicateFactoriesAutoConfiguration \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7242fd2e..a7161cd4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -75,19 +75,6 @@ spring: gateway: enabled: true global-filter.websocket-routing.enabled: true -# client-id: login-client -# client-secret: secret -# authorization-grant-type: authorization_code -# redirect-uri-template: "{baseUrl}/login/oauth2/code/{registrationId}" -# scope: openid,profile,email,resource.read -# provider: -# uaa: -# authorization-uri: http://localhost:8090/uaa/oauth/authorize -# token-uri: http://localhost:8090/uaa/oauth/token -# user-info-uri: http://localhost:8090/uaa/userinfo -# user-name-attribute: sub -# jwk-set-uri: http://localhost:8090/uaa/token_keys -# provider: uaa metrics.enabled: true default-filters: - SecureHeaders diff --git a/src/main/resources/gateway.yml b/src/main/resources/gateway.yml index cd46acd2..e181a1bf 100644 --- a/src/main/resources/gateway.yml +++ b/src/main/resources/gateway.yml @@ -27,15 +27,10 @@ georchestra: # if anonymous access is granted: # services: - cas: - target: http://cas:8080/cas/ - access-rules: - - intercept-url: /cas/login.* - anonymous: true analytics: target: http://analytics:8080/analytics/ access-rules: - - intercept-url: /analytics/.* + - intercept-url: /analytics/** allowed-roles: SUPERUSER,ORGADMIN atlas: target: http://atlas:8080/atlas/ @@ -43,40 +38,45 @@ georchestra: target: http://console:8080/console/ access-rules: - intercept-url: - - /console/public/.* - - /console/manager/public/.* + - /console/public/** + - /console/manager/public/** #/console/account resources are private except account/new and account/passwordRecovery - /console/account/new - /console/account/newPassword - /console/account/passwordRecovery - - /console/account/js/.* - - /console/account/css/.* - - /console/account/fonts/.* + - /console/account/js/** + - /console/account/css/** + - /console/account/fonts/** - /console/testPage anonymous: true - intercept-url: - - /console/private/.* - - /console/manager/.* - - /console/.*/emails - - /console/.*/sendEmail # /console/sendEmail features are reserved to SUPERUSER & delegated admins - - /console/.*/emailTemplates + - /console/private/** + - /console/manager/** + - /console/*/emails + - /console/*/sendEmail # /console/sendEmail features are reserved to SUPERUSER & delegated admins + - /console/*/emailTemplates - /console/attachments allowed-roles: SUPERUSER,ORGADMIN - intercept-url: /console/emailProxy #activated for members having the EMAILPROXY role allowed-roles: EMAILPROXY - - intercept-url: /console/internal/.* + - intercept-url: /console/internal/** allowed-roles: SUPERUSER - - intercept-url: /console/account/.* + - intercept-url: /console/account/** anonymous: false extractorapp: target: http://extractorapp:8080/extractorapp/ access-rules: - - intercept-url: /extractorapp/(admin.*|jobs/.*) + - intercept-url: /extractorapp/admin* + allowed-roles: ADMINISTRATOR + - intercept-url: /extractorapp/jobs/**) allowed-roles: ADMINISTRATOR - - intercept-url: /extractorapp/.* + - intercept-url: /extractorapp/** allowed-roles: EXTRACTORAPP geonetwork: target: http://geonetwork:8080/geonetwork/ + access-rules: + - intercept-url: /geoserver/** + anonymous: true headers: proxy: true username: false @@ -116,12 +116,12 @@ georchestra: json-user: true json-organization: true access-rules: - - intercept-url: /datafeeder/.* + - intercept-url: /datafeeder/** anonymous: false import: target: http://import:80/ access-rules: - - intercept-url: /import/.* + - intercept-url: /import/** anonymous: false --- # map to localhost at the ports defined in docker-compose.dev.yml diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..69fa00c8436c3f5db85a5cfc9f7e8e7e030c5dac GIT binary patch literal 4262 zcmeHKUu;uV82_ESi3y3}+=4n1AKEblVq$7=%feHKIw!_t8)(G%Kr-D3J}qN*>xzP! zZEl0epH<>4A!dJLRu|-t47&KBp#|5Npvl^$d(Z)EckhX^h@wpV&b@6n+YL2HB(dk- zlXJfR=YHoq-}x@UkPC-{8{_t+Kq&yL)r6-Mz(=^eyadbu&wT*rCcKv5H3ijiJqAqlob;I}aAU>@*j46pw{^I7R zed!bf?zPcTR_UBWc`1`YdmGX?9e;&!6dNv?#}%(2GH1j=FSp^|&^)3LV|Ef&Imt@{ zQq7wA4Ck!ij1z~Uf>_AJ5@OKEjOG?sz(|m(QjjsuXqiy4P6Rd)UAMa(jM5Wza56y; z8L_Bbsmt|~xNlOfOsa;Zkv`6xQbJQ77#a5qtz?ITRLks!=A~AdbmThdE0#MeR}Ts? zWg~#FtiZW@-3!iF$V((l>rwN!hJ^O+I{om92-IY;J>L{xkMJY>RVZgnu_$Q)gW z_xOf+U~EeXTVI%BIAxa?CvtX~ES>Voq+6FOyTqveZKu2v>7h60b+4!=5|oYPb-K@s z+9Ep{ETx`?)-7~3DNi+hxe-M^L(i!asE_aHwLg7HKS4Ou${q1 zyVX9|DYa-~ZZzjMqhn<|-d}$h{pABVw*5H#E. + */ + +package org.georchestra.gateway.autoconfigure.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.georchestra.gateway.filter.global.ResolveTargetGlobalFilter; +import org.georchestra.gateway.filter.headers.AddSecHeadersGatewayFilterFactory; +import org.georchestra.gateway.filter.headers.RemoveHeadersGatewayFilterFactory; +import org.georchestra.gateway.filter.headers.RemoveSecurityHeadersGatewayFilterFactory; +import org.georchestra.gateway.filter.headers.providers.GeorchestraOrganizationHeadersContributor; +import org.georchestra.gateway.filter.headers.providers.GeorchestraUserHeadersContributor; +import org.georchestra.gateway.filter.headers.providers.SecProxyHeaderContributor; +import org.georchestra.gateway.model.GatewayConfigProperties; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** + * Verify context contributions for {@link FiltersAutoConfiguration} + */ +class FiltersAutoConfigurationTest { + + private ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FiltersAutoConfiguration.class)); + + @Test + void testContext() { + runner.run(context -> { + + assertThat(context).hasSingleBean(GatewayConfigProperties.class); + assertThat(context).hasSingleBean(ResolveTargetGlobalFilter.class); + assertThat(context).hasSingleBean(AddSecHeadersGatewayFilterFactory.class); + assertThat(context).hasSingleBean(SecProxyHeaderContributor.class); + assertThat(context).hasSingleBean(GeorchestraUserHeadersContributor.class); + assertThat(context).hasSingleBean(GeorchestraOrganizationHeadersContributor.class); + assertThat(context).hasSingleBean(RemoveHeadersGatewayFilterFactory.class); + assertThat(context).hasSingleBean(RemoveSecurityHeadersGatewayFilterFactory.class); + }); + } + +} diff --git a/src/test/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfigurationTest.java b/src/test/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfigurationTest.java new file mode 100644 index 00000000..b0b12fbb --- /dev/null +++ b/src/test/java/org/georchestra/gateway/autoconfigure/app/RoutePredicateFactoriesAutoConfigurationTest.java @@ -0,0 +1,45 @@ +/* + * 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 . + */ + +package org.georchestra.gateway.autoconfigure.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.georchestra.gateway.handler.predicate.QueryParamRoutePredicateFactory; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** + * Verify context contributions for + * {@link RoutePredicateFactoriesAutoConfiguration} + */ +class RoutePredicateFactoriesAutoConfigurationTest { + + private ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RoutePredicateFactoriesAutoConfiguration.class)); + + @Test + void testContext() { + runner.run(context -> { + assertThat(context).hasSingleBean(QueryParamRoutePredicateFactory.class); + }); + } + +} diff --git a/src/test/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfigurationTest.java b/src/test/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfigurationTest.java new file mode 100644 index 00000000..5108d439 --- /dev/null +++ b/src/test/java/org/georchestra/gateway/autoconfigure/security/OAuth2SecurityAutoConfigurationTest.java @@ -0,0 +1,81 @@ +/* + * 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 . + */ + +package org.georchestra.gateway.autoconfigure.security; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.georchestra.gateway.security.oauth2.OAuth2Configuration.OAuth2AuthenticationCustomizer; +import org.georchestra.gateway.security.oauth2.OAuth2ProxyConfigProperties; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; + +/** + * Assert context contributions of {@link OAuth2SecurityAutoConfiguration} + * + */ +class OAuth2SecurityAutoConfigurationTest { + private ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OAuth2SecurityAutoConfiguration.class)); + + @Test + void testDisabledByDefault() { + testDisabled(runner); + } + + @Test + void testDisabledExplicitly() { + testDisabled(runner.withPropertyValues("georchestra.gateway.security.oauth2.enabled=false")); + } + + @Test + void testEnabled() { + runner.withPropertyValues(// + "georchestra.gateway.security.oauth2.enabled=true")// + .run(context -> { + + assertThat(context).hasSingleBean(OAuth2ProxyConfigProperties.class); + assertThat(context).hasSingleBean(OAuth2AuthenticationCustomizer.class); + assertThat(context).hasSingleBean(ReactiveOAuth2AccessTokenResponseClient.class); + assertThat(context).hasSingleBean(ReactiveOAuth2UserService.class); + assertThat(context).hasBean("oauth2WebClient"); + assertThat(context).hasBean("oAuth2AuthenticationTokenUserMapper"); + assertThat(context).hasBean("oAuth2AuthenticationTokenOpenIDUserMapper"); + }); + ; + } + + /** + * @param runner2 + */ + private void testDisabled(ApplicationContextRunner runner) { + runner.run(context -> { + assertThat(context).doesNotHaveBean(OAuth2ProxyConfigProperties.class); + assertThat(context).doesNotHaveBean(OAuth2AuthenticationCustomizer.class); + assertThat(context).doesNotHaveBean(ReactiveOAuth2AccessTokenResponseClient.class); + assertThat(context).doesNotHaveBean(ReactiveOAuth2UserService.class); + assertThat(context).doesNotHaveBean("oauth2WebClient"); + assertThat(context).doesNotHaveBean("oAuth2AuthenticationTokenUserMapper"); + assertThat(context).doesNotHaveBean("oAuth2AuthenticationTokenOpenIDUserMapper"); + }); + } +} diff --git a/src/test/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactoryTest.java b/src/test/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactoryTest.java index 90358e9d..ffca5075 100644 --- a/src/test/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactoryTest.java +++ b/src/test/java/org/georchestra/gateway/filter/headers/RemoveHeadersGatewayFilterFactoryTest.java @@ -27,7 +27,6 @@ import java.util.stream.IntStream; -import org.georchestra.gateway.filter.headers.RemoveHeadersGatewayFilterFactory; import org.georchestra.gateway.filter.headers.RemoveHeadersGatewayFilterFactory.RegExConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,6 +38,11 @@ import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; +/** + * Test suite for {@link RemoveHeadersGatewayFilterFactory}, which can remove + * any incoming request header using java regular expressions + * + */ class RemoveHeadersGatewayFilterFactoryTest { private RemoveHeadersGatewayFilterFactory filter; diff --git a/src/test/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactoryTest.java b/src/test/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactoryTest.java index ee328a92..f3733191 100644 --- a/src/test/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactoryTest.java +++ b/src/test/java/org/georchestra/gateway/filter/headers/RemoveSecurityHeadersGatewayFilterFactoryTest.java @@ -26,7 +26,6 @@ import java.util.stream.IntStream; -import org.georchestra.gateway.filter.headers.RemoveSecurityHeadersGatewayFilterFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -37,6 +36,11 @@ import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; +/** + * Test suite for {@link RemoveSecurityHeadersGatewayFilterFactory}, which + * removes all incoming {@code sec-*} headers to prevent impersonation of + * authorized users. + */ class RemoveSecurityHeadersGatewayFilterFactoryTest { private RemoveSecurityHeadersGatewayFilterFactory filter;