[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;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
|
@ -31,10 +34,117 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
|
||||
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 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
|
||||
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 relativePath = request.getRelativePath();
|
||||
String webContext = null;
|
||||
|
@ -48,7 +158,9 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
|||
} else {
|
||||
URI parsedURI = URI.create(uri);
|
||||
String path = parsedURI.getPath();
|
||||
path = path.substring(0, path.indexOf(relativePath));
|
||||
if (path.contains(relativePath)) {
|
||||
path = path.substring(0, path.indexOf(relativePath));
|
||||
}
|
||||
while (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
@ -65,31 +177,7 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
|||
}
|
||||
}
|
||||
|
||||
KeycloakDeployment deployment = cache.get(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;
|
||||
return webContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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