[KEYCLOAK-7703] HierarchicalPathBasedKeycloakConfigResolver for more fine/coarse grained Keycloak configuration in Karaf
This commit is contained in:
parent
8b6979ac18
commit
2cb7ec9432
3 changed files with 365 additions and 26 deletions
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.osgi;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This {@link KeycloakConfigResolver} tries to resolve most specific configuration for given URI path. If not found,
|
||||||
|
* <em>parent</em> path is checked up to top-level path.
|
||||||
|
*/
|
||||||
|
public class HierarchicalPathBasedKeycloakConfigResolver extends PathBasedKeycloakConfigResolver {
|
||||||
|
|
||||||
|
protected static final Logger log = Logger.getLogger(HierarchicalPathBasedKeycloakConfigResolver.class);
|
||||||
|
|
||||||
|
public HierarchicalPathBasedKeycloakConfigResolver() {
|
||||||
|
prepopulateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
|
||||||
|
// we cached all available deployments initially and now we'll try to check them from
|
||||||
|
// most specific to most general
|
||||||
|
URI uri = URI.create(request.getURI());
|
||||||
|
String path = uri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
while (path.startsWith("/")) {
|
||||||
|
path = path.substring(1);
|
||||||
|
}
|
||||||
|
String[] segments = path.split("/");
|
||||||
|
List<String> paths = collectPaths(segments);
|
||||||
|
for (String pathFragment: paths) {
|
||||||
|
KeycloakDeployment cachedDeployment = super.getCachedDeployment(pathFragment);
|
||||||
|
if (cachedDeployment != null) {
|
||||||
|
return cachedDeployment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Can't find Keycloak configuration related to URI path " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>For segments like "a, b, c, d", returns:<ul>
|
||||||
|
* <li>"a-b-c-d"</li>
|
||||||
|
* <li>"a-b-c"</li>
|
||||||
|
* <li>"a-b"</li>
|
||||||
|
* <li>"a"</li>
|
||||||
|
* <li>""</li>
|
||||||
|
* </ul></p>
|
||||||
|
* @param segments
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private List<String> collectPaths(String[] segments) {
|
||||||
|
List<String> result = new ArrayList<>(segments.length + 1);
|
||||||
|
for (int idx = segments.length; idx >= 0; idx--) {
|
||||||
|
StringBuilder sb = null;
|
||||||
|
for (int i = 0; i < idx; i++) {
|
||||||
|
if (sb == null) {
|
||||||
|
sb = new StringBuilder();
|
||||||
|
}
|
||||||
|
sb.append("-").append(segments[i]);
|
||||||
|
}
|
||||||
|
result.add(sb == null ? "" : sb.toString().substring(1));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,12 +16,15 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.osgi;
|
package org.keycloak.adapters.osgi;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.adapters.KeycloakConfigResolver;
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -31,10 +34,117 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
||||||
|
|
||||||
|
protected static final Logger log = Logger.getLogger(PathBasedKeycloakConfigResolver.class);
|
||||||
|
|
||||||
private final Map<String, KeycloakDeployment> cache = new ConcurrentHashMap<String, KeycloakDeployment>();
|
private final Map<String, KeycloakDeployment> cache = new ConcurrentHashMap<String, KeycloakDeployment>();
|
||||||
|
|
||||||
|
private File keycloakConfigLocation = null;
|
||||||
|
|
||||||
|
public PathBasedKeycloakConfigResolver() {
|
||||||
|
String location = null;
|
||||||
|
String keycloakConfig = (String) System.getProperties().get("keycloak.config");
|
||||||
|
if (keycloakConfig != null && !"".equals(keycloakConfig.trim())) {
|
||||||
|
location = keycloakConfig;
|
||||||
|
} else {
|
||||||
|
String karafEtc = (String) System.getProperties().get("karaf.etc");
|
||||||
|
if (karafEtc != null && !"".equals(karafEtc.trim())) {
|
||||||
|
location = karafEtc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (location != null) {
|
||||||
|
File loc = new File(location);
|
||||||
|
if (loc.isDirectory()) {
|
||||||
|
keycloakConfigLocation = loc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
|
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
|
||||||
|
String webContext = getDeploymentKeyForURI(request);
|
||||||
|
|
||||||
|
return getOrCreateDeployment(webContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code pathFragment} is a key for {@link KeycloakDeployment deployments}. The key is used to construct
|
||||||
|
* a path relative to {@code keycloak.config} or {@code karaf.etc} system properties.
|
||||||
|
* For given key, {@code <key>-keycloak.json} file is checked.
|
||||||
|
* @param pathFragment
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
protected synchronized KeycloakDeployment getOrCreateDeployment(String pathFragment) {
|
||||||
|
KeycloakDeployment deployment = getCachedDeployment(pathFragment);
|
||||||
|
if (null == deployment) {
|
||||||
|
// not found on the simple cache, try to load it from the file system
|
||||||
|
if (keycloakConfigLocation == null) {
|
||||||
|
throw new IllegalStateException("Neither \"keycloak.config\" nor \"karaf.etc\" java properties are set." +
|
||||||
|
" Please set one of them.");
|
||||||
|
}
|
||||||
|
|
||||||
|
File configuration = new File(keycloakConfigLocation, pathFragment + ("".equals(pathFragment) ? "" : "-")
|
||||||
|
+ "keycloak.json");
|
||||||
|
if (!cacheConfiguration(pathFragment, configuration)) {
|
||||||
|
throw new IllegalStateException("Not able to read the file " + configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected synchronized KeycloakDeployment getCachedDeployment(String pathFragment) {
|
||||||
|
return cache.get(pathFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there's a need, we can pre populate the cache of deployments.
|
||||||
|
*/
|
||||||
|
protected void prepopulateCache() {
|
||||||
|
if (keycloakConfigLocation == null || !keycloakConfigLocation.isDirectory()) {
|
||||||
|
log.warn("Can't cache Keycloak configurations. No configuration storage is accessible." +
|
||||||
|
" Please set either \"keycloak.config\" or \"karaf.etc\" system properties");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File[] configs = keycloakConfigLocation.listFiles(new FileFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File pathname) {
|
||||||
|
return pathname.isFile() && pathname.getName().endsWith("keycloak.json");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (configs != null) {
|
||||||
|
for (File config: configs) {
|
||||||
|
String pathFragment = null;
|
||||||
|
if ("keycloak.json".equals(config.getName())) {
|
||||||
|
pathFragment = "";
|
||||||
|
} else if (config.getName().endsWith("-keycloak.json")) {
|
||||||
|
pathFragment = config.getName()
|
||||||
|
.substring(0, config.getName().length() - "-keycloak.json".length());
|
||||||
|
}
|
||||||
|
cacheConfiguration(pathFragment, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean cacheConfiguration(String key, File config) {
|
||||||
|
try {
|
||||||
|
InputStream is = new FileInputStream(config);
|
||||||
|
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
|
||||||
|
cache.put(key, deployment);
|
||||||
|
return true;
|
||||||
|
} catch (FileNotFoundException | RuntimeException e) {
|
||||||
|
log.warn("Can't cache " + config + ": " + e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a context path from given {@link HttpFacade.Request}. For default context, first path segment
|
||||||
|
* is returned.
|
||||||
|
* @param request
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String getDeploymentKeyForURI(HttpFacade.Request request) {
|
||||||
String uri = request.getURI();
|
String uri = request.getURI();
|
||||||
String relativePath = request.getRelativePath();
|
String relativePath = request.getRelativePath();
|
||||||
String webContext = null;
|
String webContext = null;
|
||||||
|
@ -48,7 +158,9 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
||||||
} else {
|
} else {
|
||||||
URI parsedURI = URI.create(uri);
|
URI parsedURI = URI.create(uri);
|
||||||
String path = parsedURI.getPath();
|
String path = parsedURI.getPath();
|
||||||
|
if (path.contains(relativePath)) {
|
||||||
path = path.substring(0, path.indexOf(relativePath));
|
path = path.substring(0, path.indexOf(relativePath));
|
||||||
|
}
|
||||||
while (path.startsWith("/")) {
|
while (path.startsWith("/")) {
|
||||||
path = path.substring(1);
|
path = path.substring(1);
|
||||||
}
|
}
|
||||||
|
@ -65,31 +177,7 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeycloakDeployment deployment = cache.get(webContext);
|
return webContext;
|
||||||
if (null == deployment) {
|
|
||||||
// not found on the simple cache, try to load it from the file system
|
|
||||||
String keycloakConfig = (String) System.getProperties().get("keycloak.config");
|
|
||||||
if(keycloakConfig == null || "".equals(keycloakConfig.trim())){
|
|
||||||
String karafEtc = (String) System.getProperties().get("karaf.etc");
|
|
||||||
if(karafEtc == null || "".equals(karafEtc.trim())){
|
|
||||||
throw new IllegalStateException("Neither \"keycloak.config\" nor \"karaf.etc\" java properties are set. Please set one of them.");
|
|
||||||
}
|
|
||||||
keycloakConfig = karafEtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
String absolutePath = keycloakConfig + File.separator + webContext + ("".equals(webContext) ? "" : "-")
|
|
||||||
+ "keycloak.json";
|
|
||||||
InputStream is = null;
|
|
||||||
try {
|
|
||||||
is = new FileInputStream(absolutePath);
|
|
||||||
} catch (FileNotFoundException e){
|
|
||||||
throw new IllegalStateException("Not able to find the file " + absolutePath);
|
|
||||||
}
|
|
||||||
deployment = KeycloakDeploymentBuilder.build(is);
|
|
||||||
cache.put(webContext, deployment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deployment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.osgi;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.AuthenticationError;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.LogoutError;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
public class HierarchicalPathBasedKeycloakConfigResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void genericAndSpecificConfigurations() throws Exception {
|
||||||
|
HierarchicalPathBasedKeycloakConfigResolver resolver = new HierarchicalPathBasedKeycloakConfigResolver();
|
||||||
|
populate(resolver, true);
|
||||||
|
|
||||||
|
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/c/d/e?a=b")).getRealm(), equalTo("a-b-c-d-e"));
|
||||||
|
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/c/d/x?a=b")).getRealm(), equalTo("a-b-c-d"));
|
||||||
|
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/c/x/x?a=b")).getRealm(), equalTo("a-b-c"));
|
||||||
|
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/x/x/x?a=b")).getRealm(), equalTo("a-b"));
|
||||||
|
assertThat(resolver.resolve(new MockRequest("http://localhost/a/x/x/x/x?a=b")).getRealm(), equalTo("a"));
|
||||||
|
assertThat(resolver.resolve(new MockRequest("http://localhost/x/x/x/x/x?a=b")).getRealm(), equalTo(""));
|
||||||
|
|
||||||
|
populate(resolver, false);
|
||||||
|
try {
|
||||||
|
resolver.resolve(new MockRequest("http://localhost/x/x/x/x/x?a=b"));
|
||||||
|
fail("Expected java.lang.IllegalStateException: Can't find Keycloak configuration ...");
|
||||||
|
} catch (IllegalStateException expected) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private PathBasedKeycloakConfigResolver populate(PathBasedKeycloakConfigResolver resolver, boolean fallback)
|
||||||
|
throws Exception {
|
||||||
|
Field f = PathBasedKeycloakConfigResolver.class.getDeclaredField("cache");
|
||||||
|
f.setAccessible(true);
|
||||||
|
Map<String, KeycloakDeployment> cache = (Map<String, KeycloakDeployment>) f.get(resolver);
|
||||||
|
cache.clear();
|
||||||
|
cache.put("a-b-c-d-e", newKeycloakDeployment("a-b-c-d-e"));
|
||||||
|
cache.put("a-b-c-d", newKeycloakDeployment("a-b-c-d"));
|
||||||
|
cache.put("a-b-c", newKeycloakDeployment("a-b-c"));
|
||||||
|
cache.put("a-b", newKeycloakDeployment("a-b"));
|
||||||
|
cache.put("a", newKeycloakDeployment("a"));
|
||||||
|
if (fallback) {
|
||||||
|
cache.put("", newKeycloakDeployment(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeycloakDeployment newKeycloakDeployment(String realm) {
|
||||||
|
KeycloakDeployment deployment = new KeycloakDeployment();
|
||||||
|
deployment.setRealm(realm);
|
||||||
|
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockRequest implements OIDCHttpFacade.Request {
|
||||||
|
|
||||||
|
private String uri;
|
||||||
|
|
||||||
|
public MockRequest(String uri) {
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMethod() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getURI() {
|
||||||
|
return this.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRelativePath() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSecure() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFirstParam(String param) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getQueryParamValue(String param) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpFacade.Cookie getCookie(String cookieName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeader(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getHeaders(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteAddr() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setError(AuthenticationError error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setError(LogoutError error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue