keycloak-uncached
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 40(+5 -35)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java 170(+170 -0)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 18a93a7..92b6c88 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -17,7 +17,6 @@
*/
package org.keycloak.adapters.authorization;
-import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -31,8 +30,6 @@ import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.spi.HttpFacade.Request;
import org.keycloak.adapters.spi.HttpFacade.Response;
import org.keycloak.authorization.client.AuthzClient;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
-import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
@@ -56,7 +53,7 @@ public abstract class AbstractPolicyEnforcer {
this.policyEnforcer = policyEnforcer;
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
this.authzClient = policyEnforcer.getClient();
- this.pathMatcher = new PathMatcher();
+ this.pathMatcher = policyEnforcer.getPathMatcher();
this.paths = policyEnforcer.getPaths();
}
@@ -95,18 +92,17 @@ public abstract class AbstractPolicyEnforcer {
return createEmptyAuthorizationContext(true);
}
- PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
- Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
+ Set<String> requiredScopes = getRequiredScopes(pathConfig, request);
- if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
+ if (isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
try {
return createAuthorizationContext(accessToken);
} catch (Exception e) {
- throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
+ throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
}
- if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
+ if (!challenge(pathConfig, requiredScopes, httpFacade)) {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
response.sendError(403, "Authorization failed.");
}
@@ -226,32 +222,6 @@ public abstract class AbstractPolicyEnforcer {
};
}
- private PathConfig resolvePathConfig(PathConfig originalConfig, Request request) {
- String path = getPath(request);
-
- if (originalConfig.hasPattern()) {
- ProtectedResource resource = this.authzClient.protection().resource();
- Set<String> search = resource.findByFilter("uri=" + path);
-
- if (!search.isEmpty()) {
- // resource does exist on the server, cache it
- ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
- PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
-
- config.setScopes(originalConfig.getScopes());
- config.setMethods(originalConfig.getMethods());
- config.setParentConfig(originalConfig);
- config.setEnforcementMode(originalConfig.getEnforcementMode());
-
- this.policyEnforcer.addPath(config);
-
- return config;
- }
- }
-
- return originalConfig;
- }
-
private String getPath(Request request) {
return request.getRelativePath();
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java
new file mode 100644
index 0000000..e699203
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java
@@ -0,0 +1,170 @@
+/*
+ * 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.authorization;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PathCache {
+
+ /**
+ * The load factor.
+ */
+ private static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
+ private final Map<String, CacheEntry> cache;
+
+ private final AtomicBoolean writing = new AtomicBoolean(false);
+
+ private final long maxAge;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxEntries the maximum number of entries to keep in the cache
+ */
+ public PathCache(int maxEntries) {
+ this(maxEntries, -1);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxEntries the maximum number of entries to keep in the cache
+ * @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
+ */
+ public PathCache(final int maxEntries, long maxAge) {
+ cache = new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return cache.size() > maxEntries;
+ }
+ };
+ this.maxAge = maxAge;
+ }
+
+ public void put(String uri, PathConfig newValue) {
+ try {
+ if (parkForWriteAndCheckInterrupt()) {
+ return;
+ }
+
+ CacheEntry cacheEntry = cache.get(uri);
+
+ if (cacheEntry == null) {
+ cache.put(uri, new CacheEntry(uri, newValue, maxAge));
+ }
+ } finally {
+ writing.lazySet(false);
+ }
+ }
+
+ public PathConfig get(String uri) {
+ if (parkForReadAndCheckInterrupt()) {
+ return null;
+ }
+
+ CacheEntry cached = cache.get(uri);
+
+ if (cached != null) {
+ return removeIfExpired(cached);
+ }
+
+ return null;
+ }
+
+ public void remove(String key) {
+ try {
+ if (parkForWriteAndCheckInterrupt()) {
+ return;
+ }
+
+ cache.remove(key);
+ } finally {
+ writing.lazySet(false);
+ }
+ }
+
+ private PathConfig removeIfExpired(CacheEntry cached) {
+ if (cached == null) {
+ return null;
+ }
+
+ if (cached.isExpired()) {
+ remove(cached.key());
+ return null;
+ }
+
+ return cached.value();
+ }
+
+ private boolean parkForWriteAndCheckInterrupt() {
+ while (!writing.compareAndSet(false, true)) {
+ LockSupport.parkNanos(1L);
+ if (Thread.interrupted()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean parkForReadAndCheckInterrupt() {
+ while (writing.get()) {
+ LockSupport.parkNanos(1L);
+ if (Thread.interrupted()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static final class CacheEntry {
+
+ final String key;
+ final PathConfig value;
+ final long expiration;
+
+ CacheEntry(String key, PathConfig value, long maxAge) {
+ this.key = key;
+ this.value = value;
+ if(maxAge == -1) {
+ expiration = -1;
+ } else {
+ expiration = System.currentTimeMillis() + maxAge;
+ }
+ }
+
+ String key() {
+ return key;
+ }
+
+ PathConfig value() {
+ return value;
+ }
+
+ boolean isExpired() {
+ return expiration != -1 ? System.currentTimeMillis() > expiration : false;
+ }
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
index 8865892..d90a4fd 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
@@ -17,93 +17,206 @@
*/
package org.keycloak.adapters.authorization;
-import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
-
+import java.util.Arrays;
import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.resource.ProtectedResource;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
class PathMatcher {
- private static final String ANY_RESOURCE_PATTERN = "/*";
+ private static final char WILDCARD = '*';
+ private final AuthzClient authzClient;
+ // TODO: make this configurable
+ private PathCache cache = new PathCache(100, 30000);
- PathConfig matches(final String requestedUri, Map<String, PathConfig> paths) {
- PathConfig pathConfig = paths.get(requestedUri);
+ public PathMatcher(AuthzClient authzClient) {
+ this.authzClient = authzClient;
+ }
+
+ public PathConfig matches(final String targetUri, Map<String, PathConfig> paths) {
+ PathConfig pathConfig = paths.get(targetUri) == null ? cache.get(targetUri) : paths.get(targetUri);
if (pathConfig != null) {
return pathConfig;
}
- PathConfig actualConfig = null;
+ PathConfig matchingAnyPath = null;
+ PathConfig matchingAnySuffixPath = null;
+ PathConfig matchingPath = null;
for (PathConfig entry : paths.values()) {
- String protectedUri = entry.getPath();
- String selectedUri = null;
+ String expectedUri = entry.getPath();
+ String matchingUri = null;
- if (protectedUri.equals(ANY_RESOURCE_PATTERN) && actualConfig == null) {
- selectedUri = protectedUri;
+ if (exactMatch(expectedUri, targetUri, expectedUri)) {
+ matchingUri = expectedUri;
}
- int suffixIndex = protectedUri.indexOf(ANY_RESOURCE_PATTERN + ".");
-
- if (suffixIndex != -1) {
- String protectedSuffix = protectedUri.substring(suffixIndex + ANY_RESOURCE_PATTERN.length());
+ if (isTemplate(expectedUri)) {
+ String templateUri = buildUriFromTemplate(expectedUri, targetUri);
- if (requestedUri.endsWith(protectedSuffix)) {
- selectedUri = protectedUri;
+ if (templateUri != null) {
+ if (exactMatch(expectedUri, targetUri, templateUri)) {
+ matchingUri = templateUri;
+ entry = resolvePathConfig(entry, targetUri);
+ }
}
}
- if (protectedUri.equals(requestedUri)) {
- selectedUri = protectedUri;
- }
+ if (matchingUri != null) {
+ StringBuilder path = new StringBuilder(expectedUri);
+ int patternIndex = path.indexOf("/" + WILDCARD);
+
+ if (patternIndex != -1) {
+ path.delete(patternIndex, path.length());
+ }
- if (protectedUri.endsWith(ANY_RESOURCE_PATTERN)) {
- String formattedPattern = removeWildCardsFromUri(protectedUri);
+ patternIndex = path.indexOf("{");
- if (!formattedPattern.equals("/") && requestedUri.startsWith(formattedPattern)) {
- selectedUri = protectedUri;
+ if (patternIndex != -1) {
+ path.delete(patternIndex, path.length());
}
- if (!formattedPattern.equals("/") && formattedPattern.endsWith("/") && formattedPattern.substring(0, formattedPattern.length() - 1).equals(requestedUri)) {
- selectedUri = protectedUri;
+ String pathString = path.toString();
+
+ if ("".equals(pathString)) {
+ pathString = "/";
}
- }
- int startRegex = protectedUri.indexOf('{');
+ if (matchingUri.equals(targetUri)) {
+ cache.put(targetUri, entry);
+ return entry;
+ }
- if (startRegex != -1) {
- String prefix = protectedUri.substring(0, startRegex);
+ if (WILDCARD == expectedUri.charAt(expectedUri.length() - 1)) {
+ matchingAnyPath = entry;
+ } else {
+ int suffixIndex = expectedUri.indexOf(WILDCARD + ".");
+
+ if (suffixIndex != -1) {
+ String protectedSuffix = expectedUri.substring(suffixIndex + 1);
- if (requestedUri.startsWith(prefix)) {
- selectedUri = protectedUri;
+ if (targetUri.endsWith(protectedSuffix)) {
+ matchingAnySuffixPath = entry;
+ }
+ }
}
}
+ }
- if (selectedUri != null) {
- selectedUri = protectedUri;
- }
+ if (matchingAnySuffixPath != null) {
+ cache.put(targetUri, matchingAnySuffixPath);
+ return matchingAnySuffixPath;
+ }
- if (selectedUri != null) {
- if (actualConfig == null) {
- actualConfig = entry;
- } else {
- if (actualConfig.equals(ANY_RESOURCE_PATTERN)) {
- actualConfig = entry;
+ if (matchingAnyPath != null) {
+ cache.put(targetUri, matchingAnyPath);
+ }
+
+ return matchingAnyPath;
+ }
+
+ private boolean exactMatch(String expectedUri, String targetUri, String value) {
+ if (targetUri.equals(value)) {
+ return value.equals(targetUri);
+ }
+
+ if (endsWithWildcard(expectedUri)) {
+ return targetUri.startsWith(expectedUri.substring(0, expectedUri.length() - 2));
+ }
+
+ return false;
+ }
+
+ public String buildUriFromTemplate(String expectedUri, String targetUri) {
+ int patternStartIndex = expectedUri.indexOf("{");
+
+ if (patternStartIndex >= targetUri.length()) {
+ return null;
+ }
+
+ char[] expectedUriChars = expectedUri.toCharArray();
+ char[] matchingUri = Arrays.copyOfRange(expectedUriChars, 0, patternStartIndex);
+
+ if (Arrays.equals(matchingUri, Arrays.copyOf(targetUri.toCharArray(), matchingUri.length))) {
+ int matchingLastIndex = matchingUri.length;
+ matchingUri = Arrays.copyOf(matchingUri, targetUri.length()); // +1 so we can add a slash at the end
+ int targetPatternStartIndex = patternStartIndex;
+
+ while (patternStartIndex != -1) {
+ int parameterStartIndex = -1;
+
+ for (int i = targetPatternStartIndex; i < targetUri.length(); i++) {
+ char c = targetUri.charAt(i);
+
+ if (c != '/') {
+ if (parameterStartIndex == -1) {
+ parameterStartIndex = matchingLastIndex;
+ }
+ matchingUri[matchingLastIndex] = c;
+ matchingLastIndex++;
}
- if (protectedUri.startsWith(removeWildCardsFromUri(actualConfig.getPath()))) {
- actualConfig = entry;
+ if (c == '/' || ((i + 1 == targetUri.length()))) {
+ if (matchingUri[matchingLastIndex - 1] != '/' && matchingLastIndex < matchingUri.length) {
+ matchingUri[matchingLastIndex] = '/';
+ matchingLastIndex++;
+ }
+
+ targetPatternStartIndex = targetUri.indexOf('/', i) + 1;
+ break;
}
}
+
+ if ((patternStartIndex = expectedUri.indexOf('{', patternStartIndex + 1)) == -1) {
+ break;
+ }
+
+ if ((targetPatternStartIndex == 0 || targetPatternStartIndex == targetUri.length()) && parameterStartIndex != -1) {
+ return null;
+ }
}
+
+ return String.valueOf(matchingUri);
}
- return actualConfig;
+ return null;
+ }
+
+ public boolean endsWithWildcard(String expectedUri) {
+ return WILDCARD == expectedUri.charAt(expectedUri.length() - 1);
+ }
+
+ private boolean isTemplate(String uri) {
+ return uri.indexOf("{") != -1;
}
- private String removeWildCardsFromUri(String protectedUri) {
- return protectedUri.replaceAll("/[*]", "/");
+ private PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
+ if (originalConfig.hasPattern()) {
+ ProtectedResource resource = this.authzClient.protection().resource();
+ Set<String> search = resource.findByFilter("uri=" + path);
+
+ if (!search.isEmpty()) {
+ // resource does exist on the server, cache it
+ ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
+ PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
+
+ config.setScopes(originalConfig.getScopes());
+ config.setMethods(originalConfig.getMethods());
+ config.setParentConfig(originalConfig);
+ config.setEnforcementMode(originalConfig.getEnforcementMode());
+
+ return config;
+ }
+ }
+
+ return originalConfig;
}
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index f8a5d29..8a6a0a5 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -51,11 +51,13 @@ public class PolicyEnforcer {
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
private final Map<String, PathConfig> paths;
+ private final PathMatcher pathMatcher;
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
this.deployment = deployment;
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()));
+ this.pathMatcher = new PathMatcher(this.authzClient);
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
if (LOGGER.isDebugEnabled()) {
@@ -231,4 +233,8 @@ public class PolicyEnforcer {
return pathConfig;
}
+
+ public PathMatcher getPathMatcher() {
+ return pathMatcher;
+ }
}