Skip to content

Commit

Permalink
Merge branch 'feature/webui_shared_auth'
Browse files Browse the repository at this point in the history
  • Loading branch information
groldan committed May 21, 2024
2 parents 0971594 + 69e5207 commit 18a2f72
Show file tree
Hide file tree
Showing 17 changed files with 861 additions and 59 deletions.
2 changes: 1 addition & 1 deletion config
Submodule config updated 1 files
+7 −1 geoserver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package org.geoserver.cloud.gateway;

import org.geoserver.cloud.gateway.filter.GatewaySharedAuhenticationGlobalFilter;
import org.geoserver.cloud.gateway.filter.RouteProfileGatewayFilterFactory;
import org.geoserver.cloud.gateway.filter.StripBasePathGatewayFilterFactory;
import org.geoserver.cloud.gateway.predicate.RegExpQueryRoutePredicateFactory;
Expand Down Expand Up @@ -47,4 +48,9 @@ RouteProfileGatewayFilterFactory routeProfileGatewayFilterFactory(Environment en
StripBasePathGatewayFilterFactory stripBasePathGatewayFilterFactory() {
return new StripBasePathGatewayFilterFactory();
}

@Bean
GatewaySharedAuhenticationGlobalFilter gatewaySharedAuhenticationGlobalFilter() {
return new GatewaySharedAuhenticationGlobalFilter();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.cloud.gateway.filter;

import org.springframework.boot.web.servlet.server.Session;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;

import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Optional;

/**
* {@link GlobalFilter} to enable sharing the webui form-based authentication object with the other
* services.
*
* <p>When a user is logged in through the regular web ui's authentication form, the {@link
* Authentication} object is held in the web ui's {@link Session}. Hence, further requests to
* stateless services, as they're on separate containers, don't share the webui session, and hence
* are executed as anonymous.
*
* <p>This {@link GlobalFilter} enables a mechanism by which the authenticated user name and roles
* can be shared with the stateless services through request and response headrers, using the
* geoserver cloud gateway as the man in the middle.
*
* <p>The webui container will send a couple response headers with the authenticated user name and
* roles. The gateway will store them in its own session, and forward them to all services as
* request headers. The stateless services will intercept these request headers and impersonate the
* authenticated user as a {@link PreAuthenticatedAuthenticationToken}.
*
* <p>At the same time, the gateway will take care of removing the webui response headers from the
* responses sent to the clients, and from incoming client requests.
*
* @since 1.9
*/
public class GatewaySharedAuhenticationGlobalFilter implements GlobalFilter {

static final String X_GSCLOUD_USERNAME = "x-gsc-username";
static final String X_GSCLOUD_ROLES = "x-gsc-roles";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// first, remove any incoming header to prevent impersonation
exchange = removeRequestHeaders(exchange);

return addHeadersFromSession(exchange)
.flatMap(mutatedExchange -> proceed(mutatedExchange, chain))
.flatMap(this::saveHeadersInSession)
.flatMap(this::removeResponseHeaders);
}

private Mono<ServerWebExchange> proceed(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).thenReturn(exchange);
}

/**
* After the filter chain's run, if the proxied service replied with the user and roles headers,
* save them in the session.
*
* <p>
*
* <ul>
* <li>A missing request header does not change the session
* <li>An empty username header clears out the values in the session (i.e. it's a logout)
* </ul>
*/
private Mono<ServerWebExchange> saveHeadersInSession(ServerWebExchange exchange) {

HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
if (responseHeaders.containsKey(X_GSCLOUD_USERNAME)) {
return exchange.getSession()
.flatMap(session -> save(responseHeaders, session))
.thenReturn(exchange);
}

return Mono.just(exchange);
}

private Mono<Void> save(HttpHeaders responseHeaders, WebSession session) {
assert responseHeaders.containsKey(X_GSCLOUD_USERNAME);

Optional<String> userame =
responseHeaders.getOrDefault(X_GSCLOUD_USERNAME, List.of()).stream()
.filter(StringUtils::hasText)
.findFirst();

var roles = responseHeaders.getOrDefault(X_GSCLOUD_ROLES, List.of());

return Mono.fromRunnable(
() ->
userame.ifPresentOrElse(
user -> {
var attributes = session.getAttributes();
attributes.put(X_GSCLOUD_USERNAME, user);
attributes.put(X_GSCLOUD_ROLES, roles);
},
() -> {
var attributes = session.getAttributes();
attributes.remove(X_GSCLOUD_USERNAME);
attributes.remove(X_GSCLOUD_ROLES);
}));
}

/**
* Before proceeding with the filter chain, if the username and roles are stored in the session,
* apply the request headers for the proxied service
*/
private Mono<ServerWebExchange> addHeadersFromSession(ServerWebExchange exchange) {
return exchange.getSession().map(session -> addHeadersFromSession(session, exchange));
}

private ServerWebExchange addHeadersFromSession(
WebSession session, ServerWebExchange exchange) {

String username = session.getAttributeOrDefault(X_GSCLOUD_USERNAME, "");
if (StringUtils.hasText(username)) {
String[] roles =
session.getAttributeOrDefault(X_GSCLOUD_ROLES, List.of())
.toArray(String[]::new);
var request =
exchange.getRequest()
.mutate()
.header(X_GSCLOUD_USERNAME, username)
.header(X_GSCLOUD_ROLES, roles)
.build();
exchange = exchange.mutate().request(request).build();
}
return exchange;
}

private ServerWebExchange removeRequestHeaders(ServerWebExchange exchange) {
if (impersonating(exchange)) {
var request = exchange.getRequest().mutate().headers(this::removeHeaders).build();
exchange = exchange.mutate().request(request).build();
}
return exchange;
}

private Mono<Void> removeResponseHeaders(ServerWebExchange exchange) {
return Mono.fromRunnable(
() -> {
HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
removeHeaders(responseHeaders);
});
}

private void removeHeaders(HttpHeaders httpHeaders) {
httpHeaders.remove(X_GSCLOUD_USERNAME);
httpHeaders.remove(X_GSCLOUD_ROLES);
}

private boolean impersonating(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
return headers.containsKey(X_GSCLOUD_USERNAME) || headers.containsKey(X_GSCLOUD_ROLES);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ eureka.client:
---
# local profile, used for development only. Other settings like config and eureka urls in gs_cloud_bootstrap_profiles.yml
spring.config.activate.on-profile: local
server.port: 9000
management.server.port: 9000
server.port: 9090
management.server.port: 9090
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,28 @@
package org.geoserver.cloud.autoconfigure.authzn;

import org.geoserver.cloud.autoconfigure.security.ConditionalOnGeoServerSecurityEnabled;
import org.geoserver.cloud.security.gateway.GatewayPreAuthenticationFilter;
import org.geoserver.cloud.security.gateway.GatewayPreAuthenticationProvider;
import org.geoserver.platform.ExtensionPriority;
import org.geoserver.security.config.RequestHeaderAuthenticationFilterConfig;
import org.geoserver.cloud.autoconfigure.security.GeoServerSecurityAutoConfiguration;
import org.geoserver.cloud.security.gateway.GatewayPreAuthenticationConfiguration;
import org.geoserver.cloud.security.gateway.GatewayPreAuthenticationConfigurationWebUI;
import org.geoserver.security.web.auth.AuthenticationFilterPanelInfo;
import org.geoserver.security.web.auth.HeaderAuthFilterPanel;
import org.geoserver.security.web.auth.HeaderAuthFilterPanelInfo;
import org.geoserver.web.LoginFormInfo;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@AutoConfiguration
@Import(GatewayPreAuthenticationAutoConfiguration.GatewayPreAuthWebConfiguration.class)
// run before GeoServerSecurityAutoConfiguration so the provider is available when
// GeoServerSecurityManager calls GeoServerExtensions.extensions(GeoServerSecurityProvider.class)
@AutoConfiguration(before = GeoServerSecurityAutoConfiguration.class)
@Import({
GatewayPreAuthenticationConfiguration.class,
GatewayPreAuthenticationAutoConfiguration.WebUi.class
})
@ConditionalOnGeoServerSecurityEnabled
@SuppressWarnings("java:S1118")
public class GatewayPreAuthenticationAutoConfiguration {

@Bean
GatewayPreAuthenticationProvider gatewayPreAuthenticationProvider() {
return new GatewayPreAuthenticationProvider();
}

@Configuration
@Import(GatewayPreAuthenticationConfigurationWebUI.class)
@ConditionalOnClass(AuthenticationFilterPanelInfo.class)
static class GatewayPreAuthWebConfiguration {
@Bean
HeaderAuthFilterPanelInfo gatewayAuthPanelInfo() {
HeaderAuthFilterPanelInfo panelInfo = new HeaderAuthFilterPanelInfo();
panelInfo.setId("security.gatewayPreAuthFilter");
panelInfo.setShortTitleKey("GatewayPreAuthFilterPanel.short");
panelInfo.setTitleKey("GatewayPreAuthFilterPanel.title");
panelInfo.setDescriptionKey("GatewayPreAuthFilterPanel.description");

panelInfo.setComponentClass(HeaderAuthFilterPanel.class);
panelInfo.setServiceClass(GatewayPreAuthenticationFilter.class);
panelInfo.setServiceConfigClass(RequestHeaderAuthenticationFilterConfig.class);

return panelInfo;
}

@SuppressWarnings("unchecked")
@Bean
LoginFormInfo gatewayLoginFormInfo() {
PrioritizableLoginFormInfo lif = new PrioritizableLoginFormInfo();
lif.setPriority(ExtensionPriority.LOWEST + 1);
lif.setId("gatewayLoginFormInfo");
lif.setName("gateway");
lif.setLoginPath("/login");

@SuppressWarnings("rawtypes")
Class componentClass = GatewayPreAuthenticationAutoConfiguration.class;
lif.setComponentClass(componentClass);
lif.setIcon("oidc.png");

lif.setTitleKey("GatewayLoginFormInfo.title");
lif.setDescriptionKey("GatewayLoginFormInfo.description");
@SuppressWarnings("rawtypes")
Class class1 = GatewayPreAuthenticationFilter.class;
lif.setFilterClass(class1);
return lif;
}
}
static class WebUi {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geoserver.cloud.autoconfigure.authzn;

import lombok.Data;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Configuration properties to control enablement of the GeoServer Cloud specific "gateway shared
* authentication" mechanism, by which the authentication in the webui service is conveyed to the
* other serices using the GeoServer Cloud gateway as intermediary.
*
* @since 1.9
*/
@ConfigurationProperties(value = GatewaySharedAuthConfigProperties.PREFIX)
@Data
public class GatewaySharedAuthConfigProperties {

static final String PREFIX = "geoserver.security.gateway-shared-auth";
static final String ENABLED_PROP = PREFIX + ".enabled";
static final String AUTO_PROP = PREFIX + ".auto";
static final String SERVER_PROP = PREFIX + ".server";

/** Whether the gateway-shared-auth webui authentication conveyor protocol is enabled */
private boolean enabled = true;

/**
* Whether to automatically create the gateway-shared-auth authentication filter and append it
* to the filter chains when enabled
*/
private boolean auto = true;

/** true to act as server (i.e. to be set in the webui service) or client (default) */
private boolean server = false;
}

0 comments on commit 18a2f72

Please sign in to comment.