Skip to content

Commit 7d0af85

Browse files
authored
KEYCLOAK-19080 Simplify the RHSSO setup in an OpenShift Disconnected cluster
KEYCLOAK-19080 Simplify the RHSSO setup in an OpenShift Disconnected cluster
1 parent 7010017 commit 7d0af85

File tree

3 files changed

+111
-3
lines changed

3 files changed

+111
-3
lines changed

services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import org.apache.http.client.methods.CloseableHttpResponse;
4040
import org.apache.http.util.EntityUtils;
4141

42+
import static org.keycloak.utils.StringUtil.isBlank;
43+
4244
/**
4345
* The default {@link HttpClientFactory} for {@link HttpClientProvider HttpClientProvider's} used by Keycloak for outbound HTTP calls.
4446
* <p>
@@ -63,6 +65,10 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
6365
private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
6466
private static final String configScope = "keycloak.connectionsHttpClient.default.";
6567

68+
private static final String HTTPS_PROXY = "https_proxy";
69+
private static final String HTTP_PROXY = "http_proxy";
70+
private static final String NO_PROXY = "no_proxy";
71+
6672
private volatile CloseableHttpClient httpClient;
6773
private Config.Scope config;
6874

@@ -145,12 +151,27 @@ private void lazyInit(KeycloakSession session) {
145151
String clientKeystore = config.get("client-keystore");
146152
String clientKeystorePassword = config.get("client-keystore-password");
147153
String clientPrivateKeyPassword = config.get("client-key-password");
148-
String[] proxyMappings = config.getArray("proxy-mappings");
149154
boolean disableTrustManager = config.getBoolean("disable-trust-manager", false);
150155

151156
boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false);
152157
boolean resuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true);
153158

159+
// optionally configure proxy mappings
160+
// direct SPI config (e.g. via standalone.xml) takes precedence over env vars
161+
// lower case env vars take precedence over upper case env vars
162+
ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings"));
163+
if (proxyMappings == null || proxyMappings.isEmpty()) {
164+
logger.debug("Trying to use proxy mapping from env vars");
165+
String httpProxy = getEnvVarValue(HTTPS_PROXY);
166+
if (isBlank(httpProxy)) {
167+
httpProxy = getEnvVarValue(HTTP_PROXY);
168+
}
169+
String noProxy = getEnvVarValue(NO_PROXY);
170+
171+
logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy);
172+
proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy);
173+
}
174+
154175
HttpClientBuilder builder = new HttpClientBuilder();
155176

156177
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
@@ -161,7 +182,7 @@ private void lazyInit(KeycloakSession session) {
161182
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
162183
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
163184
.disableCookies(disableCookies)
164-
.proxyMappings(ProxyMappings.valueOf(proxyMappings))
185+
.proxyMappings(proxyMappings)
165186
.expectContinueEnabled(expectContinueEnabled)
166187
.reuseConnections(resuseConnections);
167188

@@ -215,4 +236,12 @@ private boolean getBooleanConfigWithSysPropFallback(String key, boolean defaultV
215236
return value != null ? value : defaultValue;
216237
}
217238

239+
private String getEnvVarValue(String name) {
240+
String value = System.getenv(name.toLowerCase());
241+
if (isBlank(value)) {
242+
value = System.getenv(name.toUpperCase());
243+
}
244+
return value;
245+
}
246+
218247
}

services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.jboss.logging.Logger;
2222

2323
import java.net.URI;
24+
import java.util.ArrayList;
2425
import java.util.Arrays;
2526
import java.util.Collections;
2627
import java.util.List;
@@ -30,6 +31,8 @@
3031
import java.util.regex.Pattern;
3132
import java.util.stream.Collectors;
3233

34+
import static org.keycloak.utils.StringUtil.isBlank;
35+
3336
/**
3437
* {@link ProxyMappings} describes an ordered mapping for hostname regex patterns to a {@link HttpHost} proxy.
3538
* <p>
@@ -44,9 +47,11 @@ public class ProxyMappings {
4447

4548
private static final ProxyMappings EMPTY_MAPPING = valueOf(Collections.emptyList());
4649

50+
private static final String NO_PROXY_DELIMITER = ",";
51+
4752
private final List<ProxyMapping> entries;
4853

49-
private static Map<String, ProxyMapping> hostnameToProxyCache = new ConcurrentHashMap<>();
54+
private static final Map<String, ProxyMapping> hostnameToProxyCache = new ConcurrentHashMap<>();
5055

5156
/**
5257
* Creates a {@link ProxyMappings} from the provided {@link ProxyMapping Entries}.
@@ -93,6 +98,34 @@ public static ProxyMappings valueOf(String... proxyMappings) {
9398
return valueOf(Arrays.asList(proxyMappings));
9499
}
95100

101+
/**
102+
* Creates a new {@link ProxyMappings} from provided parameters representing the established {@code HTTP(S)_PROXY}
103+
* and {@code NO_PROXY} environment variables.
104+
*
105+
* @param httpProxy a proxy used for all hosts except the ones specified in {@code noProxy}
106+
* @param noProxy a list of hosts (separated by comma) that should not use proxy;
107+
* all suffixes are matched too (e.g. redhat.com will also match access.redhat.com)
108+
* @return
109+
* @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>
110+
*/
111+
public static ProxyMappings withFixedProxyMapping(String httpProxy, String noProxy) {
112+
List<ProxyMapping> proxyMappings = new ArrayList<>();
113+
114+
if (!isBlank(httpProxy)) {
115+
// noProxy must be first as it's more specific than .*
116+
if (!isBlank(noProxy)) {
117+
for (String host : noProxy.split(NO_PROXY_DELIMITER)) {
118+
// do not support regex in no_proxy
119+
proxyMappings.add(new ProxyMapping(Pattern.compile("(?:.+\\.)?" + Pattern.quote(host)), null, null));
120+
}
121+
}
122+
123+
proxyMappings.add(ProxyMapping.valueOf(".*" + ProxyMapping.DELIMITER + httpProxy));
124+
}
125+
126+
return proxyMappings.isEmpty() ? EMPTY_MAPPING : new ProxyMappings(proxyMappings);
127+
}
128+
96129

97130
public boolean isEmpty() {
98131
return this.entries.isEmpty();

services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import static org.hamcrest.CoreMatchers.is;
3030
import static org.hamcrest.CoreMatchers.notNullValue;
3131
import static org.hamcrest.CoreMatchers.nullValue;
32+
import static org.junit.Assert.assertEquals;
33+
import static org.junit.Assert.assertNull;
3234
import static org.junit.Assert.assertThat;
3335

3436
/**
@@ -203,4 +205,48 @@ public void shouldReturnProxyAuthentication() {
203205
ProxyMapping forSalesForce = proxyMappingsWithProxyAuthen.getProxyFor("login.salesforce.com");
204206
assertThat(forSalesForce.getProxyHost().getHostName(), is("fallback"));
205207
}
208+
209+
@Test
210+
public void shouldReturnMappingForHttpProxy() {
211+
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", null);
212+
213+
ProxyMapping forGoogle = proxyMappings.getProxyFor("login.google.com");
214+
assertEquals("some-proxy.redhat.com", forGoogle.getProxyHost().getHostName());
215+
}
216+
217+
@Test
218+
public void shouldReturnMappingForHttpProxyWithNoProxy() {
219+
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "login.facebook.com");
220+
221+
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.google.com").getProxyHost().getHostName());
222+
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("facebook.com").getProxyHost().getHostName());
223+
224+
assertNull(proxyMappings.getProxyFor("login.facebook.com").getProxyHost());
225+
assertNull(proxyMappings.getProxyFor("auth.login.facebook.com").getProxyHost());
226+
}
227+
228+
@Test
229+
public void shouldReturnMappingForHttpProxyWithMultipleNoProxy() {
230+
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "login.facebook.com,corp.com");
231+
232+
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.google.com").getProxyHost().getHostName());
233+
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("facebook.com").getProxyHost().getHostName());
234+
235+
assertNull(proxyMappings.getProxyFor("login.facebook.com").getProxyHost());
236+
assertNull(proxyMappings.getProxyFor("auth.login.facebook.com").getProxyHost());
237+
assertNull(proxyMappings.getProxyFor("myapp.acme.corp.com").getProxyHost());
238+
}
239+
240+
@Test
241+
public void shouldReturnMappingForNoProxyWithInvalidChars() {
242+
ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "[lj]ogin.facebook.com");
243+
244+
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.facebook.com").getProxyHost().getHostName());
245+
assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("jogin.facebook.com").getProxyHost().getHostName());
246+
}
247+
248+
@Test
249+
public void shouldReturnEmptyMappingForEmptyHttpProxy() {
250+
assertNull(ProxyMappings.withFixedProxyMapping(null, "facebook.com"));
251+
}
206252
}

0 commit comments

Comments
 (0)