KEYCLOAK-19080 Simplify the RHSSO setup in an OpenShift Disconnected cluster

KEYCLOAK-19080 Simplify the RHSSO setup in an OpenShift Disconnected cluster
This commit is contained in:
Václav Muzikář 2021-10-18 09:35:32 +02:00 committed by GitHub
parent 7010017e0e
commit 7d0af8519b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 3 deletions

View file

@ -39,6 +39,8 @@ import java.util.concurrent.TimeUnit;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import static org.keycloak.utils.StringUtil.isBlank;
/** /**
* The default {@link HttpClientFactory} for {@link HttpClientProvider HttpClientProvider's} used by Keycloak for outbound HTTP calls. * The default {@link HttpClientFactory} for {@link HttpClientProvider HttpClientProvider's} used by Keycloak for outbound HTTP calls.
* <p> * <p>
@ -63,6 +65,10 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class); private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
private static final String configScope = "keycloak.connectionsHttpClient.default."; private static final String configScope = "keycloak.connectionsHttpClient.default.";
private static final String HTTPS_PROXY = "https_proxy";
private static final String HTTP_PROXY = "http_proxy";
private static final String NO_PROXY = "no_proxy";
private volatile CloseableHttpClient httpClient; private volatile CloseableHttpClient httpClient;
private Config.Scope config; private Config.Scope config;
@ -145,12 +151,27 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
String clientKeystore = config.get("client-keystore"); String clientKeystore = config.get("client-keystore");
String clientKeystorePassword = config.get("client-keystore-password"); String clientKeystorePassword = config.get("client-keystore-password");
String clientPrivateKeyPassword = config.get("client-key-password"); String clientPrivateKeyPassword = config.get("client-key-password");
String[] proxyMappings = config.getArray("proxy-mappings");
boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); boolean disableTrustManager = config.getBoolean("disable-trust-manager", false);
boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false); boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false);
boolean resuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true); boolean resuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true);
// optionally configure proxy mappings
// direct SPI config (e.g. via standalone.xml) takes precedence over env vars
// lower case env vars take precedence over upper case env vars
ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings"));
if (proxyMappings == null || proxyMappings.isEmpty()) {
logger.debug("Trying to use proxy mapping from env vars");
String httpProxy = getEnvVarValue(HTTPS_PROXY);
if (isBlank(httpProxy)) {
httpProxy = getEnvVarValue(HTTP_PROXY);
}
String noProxy = getEnvVarValue(NO_PROXY);
logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy);
proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy);
}
HttpClientBuilder builder = new HttpClientBuilder(); HttpClientBuilder builder = new HttpClientBuilder();
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
@ -161,7 +182,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS) .connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS) .maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
.disableCookies(disableCookies) .disableCookies(disableCookies)
.proxyMappings(ProxyMappings.valueOf(proxyMappings)) .proxyMappings(proxyMappings)
.expectContinueEnabled(expectContinueEnabled) .expectContinueEnabled(expectContinueEnabled)
.reuseConnections(resuseConnections); .reuseConnections(resuseConnections);
@ -215,4 +236,12 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
return value != null ? value : defaultValue; return value != null ? value : defaultValue;
} }
private String getEnvVarValue(String name) {
String value = System.getenv(name.toLowerCase());
if (isBlank(value)) {
value = System.getenv(name.toUpperCase());
}
return value;
}
} }

View file

@ -21,6 +21,7 @@ import org.apache.http.auth.UsernamePasswordCredentials;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.net.URI; import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -30,6 +31,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.keycloak.utils.StringUtil.isBlank;
/** /**
* {@link ProxyMappings} describes an ordered mapping for hostname regex patterns to a {@link HttpHost} proxy. * {@link ProxyMappings} describes an ordered mapping for hostname regex patterns to a {@link HttpHost} proxy.
* <p> * <p>
@ -44,9 +47,11 @@ public class ProxyMappings {
private static final ProxyMappings EMPTY_MAPPING = valueOf(Collections.emptyList()); private static final ProxyMappings EMPTY_MAPPING = valueOf(Collections.emptyList());
private static final String NO_PROXY_DELIMITER = ",";
private final List<ProxyMapping> entries; private final List<ProxyMapping> entries;
private static Map<String, ProxyMapping> hostnameToProxyCache = new ConcurrentHashMap<>(); private static final Map<String, ProxyMapping> hostnameToProxyCache = new ConcurrentHashMap<>();
/** /**
* Creates a {@link ProxyMappings} from the provided {@link ProxyMapping Entries}. * Creates a {@link ProxyMappings} from the provided {@link ProxyMapping Entries}.
@ -93,6 +98,34 @@ public class ProxyMappings {
return valueOf(Arrays.asList(proxyMappings)); return valueOf(Arrays.asList(proxyMappings));
} }
/**
* Creates a new {@link ProxyMappings} from provided parameters representing the established {@code HTTP(S)_PROXY}
* and {@code NO_PROXY} environment variables.
*
* @param httpProxy a proxy used for all hosts except the ones specified in {@code noProxy}
* @param noProxy a list of hosts (separated by comma) that should not use proxy;
* all suffixes are matched too (e.g. redhat.com will also match access.redhat.com)
* @return
* @see <a href="https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/">https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/</a>
*/
public static ProxyMappings withFixedProxyMapping(String httpProxy, String noProxy) {
List<ProxyMapping> proxyMappings = new ArrayList<>();
if (!isBlank(httpProxy)) {
// noProxy must be first as it's more specific than .*
if (!isBlank(noProxy)) {
for (String host : noProxy.split(NO_PROXY_DELIMITER)) {
// do not support regex in no_proxy
proxyMappings.add(new ProxyMapping(Pattern.compile("(?:.+\\.)?" + Pattern.quote(host)), null, null));
}
}
proxyMappings.add(ProxyMapping.valueOf(".*" + ProxyMapping.DELIMITER + httpProxy));
}
return proxyMappings.isEmpty() ? EMPTY_MAPPING : new ProxyMappings(proxyMappings);
}
public boolean isEmpty() { public boolean isEmpty() {
return this.entries.isEmpty(); return this.entries.isEmpty();

View file

@ -29,6 +29,8 @@ import java.util.List;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
/** /**
@ -203,4 +205,48 @@ public class ProxyMappingsTest {
ProxyMapping forSalesForce = proxyMappingsWithProxyAuthen.getProxyFor("login.salesforce.com"); ProxyMapping forSalesForce = proxyMappingsWithProxyAuthen.getProxyFor("login.salesforce.com");
assertThat(forSalesForce.getProxyHost().getHostName(), is("fallback")); assertThat(forSalesForce.getProxyHost().getHostName(), is("fallback"));
} }
@Test
public void shouldReturnMappingForHttpProxy() {
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", null);
ProxyMapping forGoogle = proxyMappings.getProxyFor("login.google.com");
assertEquals("some-proxy.redhat.com", forGoogle.getProxyHost().getHostName());
}
@Test
public void shouldReturnMappingForHttpProxyWithNoProxy() {
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "login.facebook.com");
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.google.com").getProxyHost().getHostName());
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("facebook.com").getProxyHost().getHostName());
assertNull(proxyMappings.getProxyFor("login.facebook.com").getProxyHost());
assertNull(proxyMappings.getProxyFor("auth.login.facebook.com").getProxyHost());
}
@Test
public void shouldReturnMappingForHttpProxyWithMultipleNoProxy() {
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "login.facebook.com,corp.com");
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.google.com").getProxyHost().getHostName());
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("facebook.com").getProxyHost().getHostName());
assertNull(proxyMappings.getProxyFor("login.facebook.com").getProxyHost());
assertNull(proxyMappings.getProxyFor("auth.login.facebook.com").getProxyHost());
assertNull(proxyMappings.getProxyFor("myapp.acme.corp.com").getProxyHost());
}
@Test
public void shouldReturnMappingForNoProxyWithInvalidChars() {
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "[lj]ogin.facebook.com");
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.facebook.com").getProxyHost().getHostName());
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("jogin.facebook.com").getProxyHost().getHostName());
}
@Test
public void shouldReturnEmptyMappingForEmptyHttpProxy() {
assertNull(ProxyMappings.withFixedProxyMapping(null, "facebook.com"));
}
} }