keycloak-uncached

Merge pull request #3956 from pedroigor/KEYCLOAK-4602 Improve

3/17/2017 4:15:56 PM

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;
+    }
 }