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 18a93a7410..92b6c88e92 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 requiredScopes = getRequiredScopes(actualPathConfig, request); + Set 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 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 0000000000..e6992039c5 --- /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 Pedro Igor + */ +public class PathCache { + + /** + * The load factor. + */ + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + + private final Map 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(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 8865892b28..d90a4fd7fd 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 Pedro Igor */ 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 paths) { - PathConfig pathConfig = paths.get(requestedUri); + public PathMatcher(AuthzClient authzClient) { + this.authzClient = authzClient; + } + + public PathConfig matches(final String targetUri, Map 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 (isTemplate(expectedUri)) { + String templateUri = buildUriFromTemplate(expectedUri, targetUri); - if (suffixIndex != -1) { - String protectedSuffix = protectedUri.substring(suffixIndex + ANY_RESOURCE_PATTERN.length()); - - if (requestedUri.endsWith(protectedSuffix)) { - selectedUri = protectedUri; - } - } - - if (protectedUri.equals(requestedUri)) { - selectedUri = protectedUri; - } - - if (protectedUri.endsWith(ANY_RESOURCE_PATTERN)) { - String formattedPattern = removeWildCardsFromUri(protectedUri); - - if (!formattedPattern.equals("/") && requestedUri.startsWith(formattedPattern)) { - selectedUri = protectedUri; - } - - if (!formattedPattern.equals("/") && formattedPattern.endsWith("/") && formattedPattern.substring(0, formattedPattern.length() - 1).equals(requestedUri)) { - selectedUri = protectedUri; - } - } - - int startRegex = protectedUri.indexOf('{'); - - if (startRegex != -1) { - String prefix = protectedUri.substring(0, startRegex); - - if (requestedUri.startsWith(prefix)) { - selectedUri = protectedUri; - } - } - - if (selectedUri != null) { - selectedUri = protectedUri; - } - - if (selectedUri != null) { - if (actualConfig == null) { - actualConfig = entry; - } else { - if (actualConfig.equals(ANY_RESOURCE_PATTERN)) { - actualConfig = entry; + if (templateUri != null) { + if (exactMatch(expectedUri, targetUri, templateUri)) { + matchingUri = templateUri; + entry = resolvePathConfig(entry, targetUri); } + } + } - if (protectedUri.startsWith(removeWildCardsFromUri(actualConfig.getPath()))) { - actualConfig = entry; + if (matchingUri != null) { + StringBuilder path = new StringBuilder(expectedUri); + int patternIndex = path.indexOf("/" + WILDCARD); + + if (patternIndex != -1) { + path.delete(patternIndex, path.length()); + } + + patternIndex = path.indexOf("{"); + + if (patternIndex != -1) { + path.delete(patternIndex, path.length()); + } + + String pathString = path.toString(); + + if ("".equals(pathString)) { + pathString = "/"; + } + + if (matchingUri.equals(targetUri)) { + cache.put(targetUri, entry); + return entry; + } + + 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 (targetUri.endsWith(protectedSuffix)) { + matchingAnySuffixPath = entry; + } } } } } - return actualConfig; + if (matchingAnySuffixPath != null) { + cache.put(targetUri, matchingAnySuffixPath); + return matchingAnySuffixPath; + } + + if (matchingAnyPath != null) { + cache.put(targetUri, matchingAnyPath); + } + + return matchingAnyPath; } - private String removeWildCardsFromUri(String protectedUri) { - return protectedUri.replaceAll("/[*]", "/"); + 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 (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 null; + } + + public boolean endsWithWildcard(String expectedUri) { + return WILDCARD == expectedUri.charAt(expectedUri.length() - 1); + } + + private boolean isTemplate(String uri) { + return uri.indexOf("{") != -1; + } + + private PathConfig resolvePathConfig(PathConfig originalConfig, String path) { + if (originalConfig.hasPattern()) { + ProtectedResource resource = this.authzClient.protection().resource(); + Set 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 f8a5d295e5..8a6a0a5bb1 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 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; + } }