Skip to content

Commit

Permalink
Merge pull request #142 from georchestra/custom-headers-config-issue-120
Browse files Browse the repository at this point in the history
headers filters - custom service configuration should override default headers configuration (#120)
  • Loading branch information
pmauduit committed Sep 12, 2024
2 parents 36f3a60 + 6fd609d commit cecbf43
Show file tree
Hide file tree
Showing 13 changed files with 433 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,40 @@
import java.util.HashSet;
import java.util.Set;

import com.google.common.annotations.VisibleForTesting;
import org.json.JSONObject;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;

import lombok.extern.slf4j.Slf4j;

//TODO: remove class as dead code?
@Slf4j(topic = "org.georchestra.gateway.events")
@Slf4j
public class RabbitmqEventsListener implements MessageListener {

public static final String OAUTH2_ACCOUNT_CREATION_RECEIVED = "OAUTH2-ACCOUNT-CREATION-RECEIVED";

private static Set<String> synReceivedMessageUid = Collections.synchronizedSet(new HashSet<String>());

public void onMessage(Message message) {
String messageBody = new String(message.getBody());
JSONObject jsonObj = new JSONObject(messageBody);
String uid = jsonObj.getString("uid");
String subject = jsonObj.getString("subject");
if (subject.equals(OAUTH2_ACCOUNT_CREATION_RECEIVED)
&& !synReceivedMessageUid.stream().anyMatch(s -> s.equals(uid))) {
String msg = jsonObj.getString("msg");
synReceivedMessageUid.add(uid);
log.info(msg);
try {
String messageBody = new String(message.getBody());
JSONObject jsonObj = new JSONObject(messageBody);
String uid = jsonObj.getString("uid");
String subject = jsonObj.getString("subject");
if (subject.equals(OAUTH2_ACCOUNT_CREATION_RECEIVED)
&& !synReceivedMessageUid.stream().anyMatch(s -> s.equals(uid))) {
String msg = jsonObj.getString("msg");
synReceivedMessageUid.add(uid);
log.info(msg);
}

} catch (Exception e) {
log.error("Exception caught when evaluating a message from RabbitMq, it will be silently discarded.", e);
}
}

@VisibleForTesting
public static Set<String> getSynReceivedMessageUid() {
return synReceivedMessageUid;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

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

import org.georchestra.gateway.model.GatewayConfigProperties;
import org.georchestra.gateway.model.GeorchestraTargetConfig;
import org.georchestra.gateway.model.HeaderMappings;
import org.georchestra.gateway.model.RoleBasedAccessRule;
import org.georchestra.gateway.model.Service;
import org.georchestra.gateway.security.ResolveGeorchestraUserGlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
Expand Down Expand Up @@ -75,32 +79,57 @@ public class ResolveTargetGlobalFilter implements GlobalFilter, Ordered {
Route route = (Route) exchange.getAttributes().get(GATEWAY_ROUTE_ATTR);
Objects.requireNonNull(route, "no route matched, filter shouldn't be hit");

GeorchestraTargetConfig config = resolveTarget(route);
GeorchestraTargetConfig targetConfig = resolveTarget(route);
log.debug("Storing geOrchestra target config for Route {} request context", route.getId());
GeorchestraTargetConfig.setTarget(exchange, config);
GeorchestraTargetConfig.setTarget(exchange, targetConfig);
return chain.filter(exchange);
}

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

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

Optional<Service> service = findService(route);

setAccessRules(target, service);
setHeaderMappings(target, service);

return target;
}

private void setAccessRules(GeorchestraTargetConfig target, Optional<Service> service) {
List<RoleBasedAccessRule> globalAccessRules = config.getGlobalAccessRules();
var targetAccessRules = service.map(Service::getAccessRules).filter(Objects::nonNull).filter(l -> !l.isEmpty())
.orElse(globalAccessRules);

target.accessRules(targetAccessRules);
}

private void setHeaderMappings(GeorchestraTargetConfig target, Optional<Service> service) {
HeaderMappings defaultHeaders = config.getDefaultHeaders();
HeaderMappings mergedHeaders = service.flatMap(Service::headers)
.map(serviceHeaders -> merge(defaultHeaders, serviceHeaders)).orElse(defaultHeaders);

target.headers(mergedHeaders);
}

private HeaderMappings merge(HeaderMappings defaults, HeaderMappings service) {
return defaults.copy().merge(service);
}

private Optional<Service> findService(@NonNull Route route) {
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 Optional.of(service);
}
}
return target;

return Optional.empty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
public class AddSecHeadersGatewayFilterFactory
extends AbstractGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {

public static String DISABLE_SECURITY_HEADERS = AddSecHeadersGatewayFilterFactory.class.getName()
+ ".DISABLE_SECURITY_HEADERS";
public static final String DISABLE_SECURITY_HEADERS = "%s.DISABLE_SECURITY_HEADERS"
.formatted(AddSecHeadersGatewayFilterFactory.class.getName());

private final List<HeaderContributor> providers;

public AddSecHeadersGatewayFilterFactory(List<HeaderContributor> providers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.georchestra.gateway.filter.headers.providers;

import java.io.UncheckedIOException;
import java.util.Optional;
import java.util.function.Consumer;

Expand Down Expand Up @@ -64,21 +65,20 @@ public JsonPayloadHeadersContributor() {
}

public @Override Consumer<HttpHeaders> prepare(ServerWebExchange exchange) {
return headers -> {
GeorchestraTargetConfig.getTarget(exchange)//
.map(GeorchestraTargetConfig::headers)//
.ifPresent(mappings -> {
Optional<GeorchestraUser> user = GeorchestraUsers.resolve(exchange);
Optional<Organization> org = GeorchestraOrganizations.resolve(exchange);
return headers -> GeorchestraTargetConfig.getTarget(exchange)//
.map(GeorchestraTargetConfig::headers)//
.ifPresent(mappings -> addJsonPayloads(exchange, mappings, headers));
}

addJson(headers, "sec-user", mappings.getJsonUser(), user);
addJson(headers, "sec-organization", mappings.getJsonOrganization(), org);
});
};
private void addJsonPayloads(final ServerWebExchange exchange, final HeaderMappings mappings, HttpHeaders headers) {
Optional<GeorchestraUser> user = GeorchestraUsers.resolve(exchange);
Optional<Organization> org = GeorchestraOrganizations.resolve(exchange);
addJson(headers, "sec-user", mappings.getJsonUser().orElse(false), user);
addJson(headers, "sec-organization", mappings.getJsonOrganization().orElse(false), org);
}

private void addJson(HttpHeaders target, String headerName, Optional<Boolean> enabled, Optional<?> toEncode) {
if (enabled.orElse(false)) {
private void addJson(HttpHeaders target, String headerName, boolean enabled, Optional<?> toEncode) {
if (enabled) {
toEncode.map(this::encodeJson)//
.map(this::encodeBase64)//
.ifPresent(encoded -> target.add(headerName, encoded));
Expand All @@ -89,7 +89,7 @@ private String encodeJson(Object payloadObject) {
try {
return this.encoder.writer().writeValueAsString(payloadObject);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
throw new UncheckedIOException(e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import org.georchestra.security.model.Organization;
import org.springframework.web.server.ServerWebExchange;

import lombok.experimental.UtilityClass;

@UtilityClass
public class GeorchestraOrganizations {

static final String GEORCHESTRA_ORGANIZATION_KEY = GeorchestraOrganizations.class.getCanonicalName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import org.springframework.web.server.ServerWebExchange;

import lombok.NonNull;
import lombok.experimental.UtilityClass;

@UtilityClass
public class GeorchestraUsers {

static final String GEORCHESTRA_USER_KEY = GeorchestraUsers.class.getCanonicalName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,57 @@ private void setAll(Optional<Boolean> val) {
this.orgLastUpdated = val;
this.jsonOrganization = val;
}

public HeaderMappings userid(boolean b) {
setUserid(Optional.of(b));
return this;
}

public HeaderMappings jsonUser(boolean b) {
setJsonUser(Optional.of(b));
return this;
}

public HeaderMappings jsonOrganization(boolean b) {
setJsonOrganization(Optional.of(b));
return this;
}

/**
* @return a copy of this object
*/
public HeaderMappings copy() {
HeaderMappings copy = new HeaderMappings();
copy.merge(this);
return copy;
}

/**
* Applies the non-empty fields from {@code other} to this one, and returns this
*/
public HeaderMappings merge(HeaderMappings other) {
proxy = merge(proxy, other.proxy);
userid = merge(userid, other.userid);
lastUpdated = merge(lastUpdated, other.lastUpdated);
username = merge(username, other.username);
roles = merge(roles, other.roles);
org = merge(org, other.org);
email = merge(email, other.email);
firstname = merge(firstname, other.firstname);
lastname = merge(lastname, other.lastname);
tel = merge(tel, other.tel);
address = merge(address, other.address);
title = merge(title, other.title);
notes = merge(notes, other.notes);
jsonUser = merge(jsonUser, other.jsonUser);
orgname = merge(orgname, other.orgname);
orgid = merge(orgid, other.orgid);
orgLastUpdated = merge(orgLastUpdated, other.orgLastUpdated);
jsonOrganization = merge(jsonOrganization, other.jsonOrganization);
return this;
}

private Optional<Boolean> merge(Optional<Boolean> a, Optional<Boolean> b) {
return b.isEmpty() ? a : b;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ public class Service {
/**
* Service-specific security headers configuration
*/
private Optional<HeaderMappings> headers = Optional.empty();
private HeaderMappings headers;

/**
* List of Ant-pattern based access rules for the given back-end service
*/
private List<RoleBasedAccessRule> accessRules = List.of();

public Optional<HeaderMappings> headers() {
return Optional.ofNullable(headers);
}
}
Loading

0 comments on commit cecbf43

Please sign in to comment.