Skip to content

Commit

Permalink
fix: Fix signing outgoing call from Cloud Gateway just if necessary (#…
Browse files Browse the repository at this point in the history
…3203)

Signed-off-by: Pavel Jareš <[email protected]>
  • Loading branch information
pj892031 authored Nov 23, 2023
1 parent c9d9bfd commit 12ca262
Show file tree
Hide file tree
Showing 22 changed files with 523 additions and 220 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,20 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
import org.springframework.cloud.gateway.config.HttpClientCustomizer;
import org.springframework.cloud.gateway.config.HttpClientProperties;
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
Expand All @@ -44,7 +48,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
Expand Down Expand Up @@ -147,7 +151,6 @@ public void updateConfigParameters() {
}
}


public HttpsFactory factory() {
HttpsConfig config = HttpsConfig.builder()
.protocol(protocol)
Expand All @@ -162,26 +165,56 @@ public HttpsFactory factory() {
return new HttpsFactory(config);
}

/**
* This bean processor is used to override bean routingFilter defined at
* org.springframework.cloud.gateway.config.GatewayAutoConfiguration.NettyConfiguration#routingFilter(HttpClient, ObjectProvider, HttpClientProperties)
*
* There is no simple way how to override this specific bean, but bean processing could handle that.
*
* @param httpClient default http client
* @param headersFiltersProvider header filter for spring cloud gateway router
* @param properties client HTTP properties
* @return bean processor to replace NettyRoutingFilter by NettyRoutingFilterApiml
*/
@Bean
HttpClientCustomizer secureCustomizer() {
return httpClient -> httpClient.secure(b -> b.sslContext(sslContext()));
public BeanPostProcessor routingFilterHandler(HttpClient httpClient, ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider, HttpClientProperties properties) {
// obtain SSL contexts (one with keystore to support client cert sign and truststore, second just with truststore)
SslContext justTruststore = sslContext(false);
SslContext withKeystore = sslContext(true);

return new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if ("routingFilter".equals(beanName)) {
// once is creating original bean by autoconfiguration replace it with custom implementation
return new NettyRoutingFilterApiml(httpClient, headersFiltersProvider, properties, justTruststore, withKeystore);
}
// do not touch any other bean
return bean;
}
};
}


/**
* @return io.netty.handler.ssl.SslContext for http client.
*/
SslContext sslContext() {
SslContext sslContext(boolean setKeystore) {
try {
KeyStore keyStore = SecurityUtils.loadKeyStore(keyStoreType, keyStorePath, keyStorePassword);
KeyStore trustStore = SecurityUtils.loadKeyStore(trustStoreType, trustStorePath, trustStorePassword);
SslContextBuilder builder = SslContextBuilder.forClient();

KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);
KeyStore trustStore = SecurityUtils.loadKeyStore(trustStoreType, trustStorePath, trustStorePassword);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

trustManagerFactory.init(trustStore);
return SslContextBuilder.forClient().keyManager(keyManagerFactory).trustManager(trustManagerFactory).build();
builder.trustManager(trustManagerFactory);

if (setKeystore) {
KeyStore keyStore = SecurityUtils.loadKeyStore(keyStoreType, keyStorePath, keyStorePassword);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);
builder.keyManager(keyManagerFactory);
}

return builder.build();
} catch (Exception e) {
apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage());
throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e,
Expand Down Expand Up @@ -216,7 +249,7 @@ public CloudEurekaClient primaryEurekaClient(ApplicationInfoManager manager, Eur
}

@Bean
public List<AdditionalRegistration> additionalRegistration(StandardEnvironment environment) {
public List<AdditionalRegistration> additionalRegistration() {
List<AdditionalRegistration> additionalRegistrations = new AdditionalRegistrationParser().extractAdditionalRegistrations(System.getenv());
log.debug("Parsed {} additional registration: {}", additionalRegistrations.size(), additionalRegistrations);
return additionalRegistrations;
Expand Down Expand Up @@ -267,10 +300,15 @@ public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer()
}

@Bean
public WebClient webClient() {
HttpClient client = HttpClient.create().secure(ssl -> ssl.sslContext(sslContext()));
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(client)).build();
@Primary
public WebClient webClient(HttpClient httpClient) {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}

@Bean
public WebClient webClientClientCert(HttpClient httpClient) {
httpClient = httpClient.secure(sslContextSpec -> sslContextSpec.sslContext(sslContext(true)));
return webClient(httpClient);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.cloudgatewayservice.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.SslContext;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.config.HttpClientProperties;
import org.springframework.cloud.gateway.filter.NettyRoutingFilter;
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.web.server.ServerWebExchange;
import reactor.netty.http.client.HttpClient;

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

import static org.springframework.cloud.gateway.support.RouteMetadataUtils.CONNECT_TIMEOUT_ATTR;
import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE;

public class NettyRoutingFilterApiml extends NettyRoutingFilter {

private final HttpClient httpClientNoCert;
private final HttpClient httpClientClientCert;

public NettyRoutingFilterApiml(
HttpClient httpClient,
ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider,
HttpClientProperties properties,
SslContext justTruststore,
SslContext withKeystore
) {
super(null, headersFiltersProvider, properties);

// construct http clients with different SSL configuration - with / without client certs
httpClientNoCert = httpClient.secure(sslContextSpec -> sslContextSpec.sslContext(justTruststore));
httpClientClientCert = httpClient.secure(sslContextSpec -> sslContextSpec.sslContext(withKeystore));
}

static Integer getInteger(Object connectTimeoutAttr) {
Integer connectTimeout;
if (connectTimeoutAttr instanceof Integer) {
connectTimeout = (Integer) connectTimeoutAttr;
}
else {
connectTimeout = Integer.parseInt(connectTimeoutAttr.toString());
}
return connectTimeout;
}

@Override
protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
// select proper HttpClient instance by attribute apiml.useClientCert
boolean useClientCert = Optional.ofNullable((Boolean) exchange.getAttribute(HTTP_CLIENT_USE_CLIENT_CERTIFICATE)).orElse(Boolean.FALSE);
HttpClient httpClient = useClientCert ? httpClientClientCert : httpClientNoCert;

Object connectTimeoutAttr = route.getMetadata().get(CONNECT_TIMEOUT_ATTR);
if (connectTimeoutAttr != null) {
// if there is configured timeout, respect it
Integer connectTimeout = getInteger(connectTimeoutAttr);
return httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout);
}

// otherwise just return selected HttpClient
return httpClient;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ public class RoutingConfig {
@Value("${apiml.service.ignoredHeadersWhenCorsEnabled:-}")
private String ignoredHeadersWhenCorsEnabled;

@Value("${apiml.service.forwardClientCertEnabled:false}")
private String forwardingClientCertEnabled;

@Bean
public List<FilterDefinition> filters() {
FilterDefinition circuitBreakerFilter = new FilterDefinition();
Expand All @@ -39,14 +36,9 @@ public List<FilterDefinition> filters() {
retryFilter.addArg("retries", "5");
retryFilter.addArg("statuses", "SERVICE_UNAVAILABLE");

FilterDefinition clientCertFilter = new FilterDefinition();
clientCertFilter.setName("ClientCertFilterFactory");
clientCertFilter.addArg("forwardingEnabled", forwardingClientCertEnabled);

List<FilterDefinition> filters = new ArrayList<>();
filters.add(circuitBreakerFilter);
filters.add(retryFilter);
filters.add(clientCertFilter);
for (String headerName : ignoredHeadersWhenCorsEnabled.split(",")) {
FilterDefinition removeHeaders = new FilterDefinition();
removeHeaders.setName("RemoveRequestHeader");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.zowe.apiml.cloudgatewayservice.filters.ClientCertFilterFactory.CLIENT_CERT_HEADER;
import static org.zowe.apiml.constants.ApimlConstants.PAT_COOKIE_AUTH_NAME;
import static org.zowe.apiml.constants.ApimlConstants.PAT_HEADER_NAME;
import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME;
Expand Down Expand Up @@ -140,6 +141,7 @@ public abstract class AbstractAuthSchemeFactory<T extends AbstractAuthSchemeFact
StringUtils.equalsIgnoreCase(headerName, "X-Certificate-Public") ||
StringUtils.equalsIgnoreCase(headerName, "X-Certificate-DistinguishedName") ||
StringUtils.equalsIgnoreCase(headerName, "X-Certificate-CommonName") ||
StringUtils.equalsIgnoreCase(headerName, CLIENT_CERT_HEADER) ||
StringUtils.equalsIgnoreCase(headerName, HttpHeaders.COOKIE);

private static final RobinRoundIterator<ServiceInstance> robinRound = new RobinRoundIterator<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

package org.zowe.apiml.cloudgatewayservice.filters;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
Expand All @@ -22,6 +21,8 @@
import java.security.cert.X509Certificate;
import java.util.Base64;

import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE;

/**
* Objective is to include new header in the request which contains incoming client certificate
* so that further processing (mapping to mainframe userId) is possible by the domain gateway.
Expand All @@ -30,7 +31,7 @@
@Slf4j
public class ClientCertFilterFactory extends AbstractGatewayFilterFactory<ClientCertFilterFactory.Config> {

private static final String CLIENT_CERT_HEADER = "Client-Cert";
public static final String CLIENT_CERT_HEADER = "Client-Cert";

public ClientCertFilterFactory() {
super(Config.class);
Expand All @@ -49,11 +50,12 @@ public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate().headers(headers -> {
headers.remove(CLIENT_CERT_HEADER);
if (config.isForwardingEnabled() && exchange.getRequest().getSslInfo() != null) {
if (exchange.getRequest().getSslInfo() != null) {
X509Certificate[] certificates = exchange.getRequest().getSslInfo().getPeerCertificates();
if (certificates != null && certificates.length > 0) {
try {
final String encodedCert = Base64.getEncoder().encodeToString(certificates[0].getEncoded());
exchange.getAttributes().put(HTTP_CLIENT_USE_CLIENT_CERTIFICATE, Boolean.TRUE);
headers.add(CLIENT_CERT_HEADER, encodedCert);
log.debug("Incoming client certificate has been added to the {} header.", CLIENT_CERT_HEADER);
} catch (CertificateEncodingException e) {
Expand All @@ -67,12 +69,8 @@ public GatewayFilter apply(Config config) {
});
}

@SuppressWarnings("squid:S2094")
public static class Config {
@Setter
private String forwardingEnabled;

public boolean isForwardingEnabled() {
return Boolean.parseBoolean(forwardingEnabled);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.http.HttpHeaders;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
Expand All @@ -38,7 +39,7 @@ public class PassticketFilterFactory extends AbstractAuthSchemeFactory<Passticke
private static final String TICKET_URL = "%s://%s:%s/%s/api/v1/auth/ticket";
private static final ObjectWriter WRITER = new ObjectMapper().writer();

public PassticketFilterFactory(WebClient webClient, InstanceInfoService instanceInfoService, MessageService messageService) {
public PassticketFilterFactory(@Qualifier("webClientClientCert") WebClient webClient, InstanceInfoService instanceInfoService, MessageService messageService) {
super(Config.class, webClient, instanceInfoService, messageService);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import java.security.cert.X509Certificate;
import java.util.Base64;

import static org.zowe.apiml.constants.ApimlConstants.HTTP_CLIENT_USE_CLIENT_CERTIFICATE;

@Service
@Slf4j
public class X509FilterFactory extends AbstractGatewayFilterFactory<X509FilterFactory.Config> {
Expand All @@ -51,6 +53,7 @@ public GatewayFilter apply(Config config) {
if (certificates != null && certificates.length > 0) {
ServerHttpRequest request = exchange.getRequest().mutate().headers(headers -> {
try {
exchange.getAttributes().put(HTTP_CLIENT_USE_CLIENT_CERTIFICATE, Boolean.TRUE);
setHeader(headers, config.getHeaders().split(","), certificates[0]);
} catch (CertificateEncodingException | InvalidNameException e) {
headers.add(ApimlConstants.AUTH_FAIL_HEADER, "Invalid client certificate in request. Error message: " + e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package org.zowe.apiml.cloudgatewayservice.filters;

import lombok.EqualsAndHashCode;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
Expand All @@ -32,7 +33,7 @@ public class ZosmfFilterFactory extends AbstractAuthSchemeFactory<ZosmfFilterFac

private static final String ZOSMF_URL = "%s://%s:%d/%s/zaas/zosmf";

public ZosmfFilterFactory(WebClient webClient, InstanceInfoService instanceInfoService, MessageService messageService) {
public ZosmfFilterFactory(@Qualifier("webClientClientCert") WebClient webClient, InstanceInfoService instanceInfoService, MessageService messageService) {
super(Config.class, webClient, instanceInfoService, messageService);
}

Expand Down
Loading

0 comments on commit 12ca262

Please sign in to comment.