Merge pull request #5120 from pedroigor/KEYCLOAK-7029

[KEYCLOAK-7029] - Configuration of cache policies for cached resources/path
This commit is contained in:
Pedro Igor 2018-04-05 09:33:23 -03:00 committed by GitHub
commit e1f5245145
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 402 additions and 56 deletions

View file

@ -159,7 +159,7 @@ public abstract class AbstractPolicyEnforcer {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
}
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
policyEnforcer.getPaths().remove(actualPathConfig);
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
}
return true;
}
@ -281,7 +281,7 @@ public abstract class AbstractPolicyEnforcer {
}
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
return new ClientAuthorizationContext(accessToken, pathConfig, policyEnforcer.getPaths(), getAuthzClient());
return new ClientAuthorizationContext(accessToken, pathConfig, getAuthzClient());
}
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {

View file

@ -40,6 +40,7 @@ public class PathCache {
private final AtomicBoolean writing = new AtomicBoolean(false);
private final long maxAge;
private final boolean enabled;
/**
* Creates a new instance.
@ -55,9 +56,14 @@ public class PathCache {
}
};
this.maxAge = maxAge;
this.enabled = maxAge > 0;
}
public void put(String uri, PathConfig newValue) {
if (!enabled) {
return;
}
try {
if (parkForWriteAndCheckInterrupt()) {
return;

View file

@ -38,6 +38,7 @@ import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.common.util.PathMatcher;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
@ -52,8 +53,8 @@ public class PolicyEnforcer {
private final KeycloakDeployment deployment;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
private final PathConfigMatcher pathMatcher;
private final Map<String, PathConfig> paths;
private final PathMatcher pathMatcher;
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
this.deployment = deployment;
@ -70,8 +71,8 @@ public class PolicyEnforcer {
}
});
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
this.pathMatcher = createPathMatcher(authzClient);
paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
pathMatcher = new PathConfigMatcher(paths, enforcerConfig, authzClient);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configurations:");
@ -117,7 +118,7 @@ public class PolicyEnforcer {
return paths;
}
public PathMatcher<PathConfig> getPathMatcher() {
public PathConfigMatcher getPathMatcher() {
return pathMatcher;
}
@ -216,10 +217,26 @@ public class PolicyEnforcer {
return paths;
}
private PathMatcher<PathConfig> createPathMatcher(final AuthzClient authzClient) {
final PathCache pathCache = new PathCache(100, 30000);
public class PathConfigMatcher extends PathMatcher<PathConfig> {
private final Map<String, PathConfig> paths;
private final PathCache pathCache;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
public PathConfigMatcher(Map<String, PathConfig> paths, PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
this.paths = paths;
this.enforcerConfig = enforcerConfig;
PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
if (cacheConfig == null) {
cacheConfig = new PathCacheConfig();
}
pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan());
this.authzClient = authzClient;
}
return new PathMatcher<PathConfig>() {
@Override
public PathConfig matches(String targetUri) {
PathConfig pathConfig = pathCache.get(targetUri);
@ -230,19 +247,20 @@ public class PolicyEnforcer {
pathConfig = super.matches(targetUri);
if (enforcerConfig.getLazyLoadPaths() && (pathConfig == null || pathConfig.getPath().contains("*"))) {
if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) {
if ((pathConfig == null || (pathConfig.getPath().contains("*")))) {
try {
List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
if (!matchingResources.isEmpty()) {
pathConfig = PathConfig.createPathConfig(matchingResources.get(0));
paths.put(pathConfig.getPath(), pathConfig);
}
} catch (Exception cause) {
LOGGER.errorf(cause, "Could not lazy load paths from server");
LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
return null;
}
}
}
pathCache.put(targetUri, pathConfig);
@ -266,7 +284,6 @@ public class PolicyEnforcer {
List<ResourceRepresentation> search = resource.findByUri(path);
if (!search.isEmpty()) {
// resource does exist on the server, cache it
ResourceRepresentation targetResource = search.get(0);
PathConfig config = PathConfig.createPathConfig(targetResource);
@ -281,6 +298,9 @@ public class PolicyEnforcer {
return null;
}
public void removeFromCache(String pathConfig) {
pathCache.remove(pathConfig);
}
};
}
}

View file

@ -30,8 +30,8 @@ public class ClientAuthorizationContext extends AuthorizationContext {
private final AuthzClient client;
public ClientAuthorizationContext(AccessToken authzToken, PolicyEnforcerConfig.PathConfig current, Map<String, PolicyEnforcerConfig.PathConfig> paths, AuthzClient client) {
super(authzToken, current, paths);
public ClientAuthorizationContext(AccessToken authzToken, PolicyEnforcerConfig.PathConfig current, AuthzClient client) {
super(authzToken, current);
this.client = client;
}

View file

@ -33,18 +33,16 @@ public class AuthorizationContext {
private final AccessToken authzToken;
private final PathConfig current;
private final Map<String, PathConfig> paths;
private boolean granted;
public AuthorizationContext(AccessToken authzToken, PathConfig current, Map<String, PathConfig> paths) {
public AuthorizationContext(AccessToken authzToken, PathConfig current) {
this.authzToken = authzToken;
this.current = current;
this.paths = paths;
this.granted = true;
}
public AuthorizationContext() {
this(null, null, null);
this(null, null);
this.granted = false;
}

View file

@ -38,6 +38,10 @@ public class PolicyEnforcerConfig {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PathConfig> paths = new ArrayList<>();
@JsonProperty("path-cache")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private PathCacheConfig pathCacheConfig;
@JsonProperty("lazy-load-paths")
private Boolean lazyLoadPaths = Boolean.FALSE;
@ -53,6 +57,10 @@ public class PolicyEnforcerConfig {
return this.paths;
}
public PathCacheConfig getPathCacheConfig() {
return pathCacheConfig;
}
public Boolean getLazyLoadPaths() {
return lazyLoadPaths;
}
@ -77,6 +85,10 @@ public class PolicyEnforcerConfig {
this.paths = paths;
}
public void setPathCacheConfig(PathCacheConfig pathCacheConfig) {
this.pathCacheConfig = pathCacheConfig;
}
public String getOnDenyRedirectTo() {
return onDenyRedirectTo;
}
@ -250,6 +262,29 @@ public class PolicyEnforcerConfig {
}
}
public static class PathCacheConfig {
@JsonProperty("max-entries")
int maxEntries = 1000;
long lifespan = 30000;
public int getMaxEntries() {
return maxEntries;
}
public void setMaxEntries(int maxEntries) {
this.maxEntries = maxEntries;
}
public long getLifespan() {
return lifespan;
}
public void setLifespan(long lifespan) {
this.lifespan = lifespan;
}
}
public enum EnforcementMode {
PERMISSIVE,
ENFORCING,

View file

@ -99,6 +99,12 @@ public class AbstractPolicyRepresentation {
this.policies.addAll(Arrays.asList(id));
}
public void removePolicy(String policy) {
if (policies != null) {
policies.remove(policy);
}
}
public Set<String> getResources() {
return resources;
}

View file

@ -0,0 +1,26 @@
{
"realm": "servlet-authz",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url" : "http://localhost:8180/auth",
"ssl-required" : "external",
"resource" : "servlet-authz-app",
"public-client" : false,
"credentials": {
"secret": "secret"
},
"policy-enforcer": {
"on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp",
"path-cache": {
"lifespan": 0,
"max-entries": 1000
},
"paths": [
{
"name": "Premium Resource",
"path": "/protected/premium/pep-disabled.jsp",
"enforcement-mode": "DISABLED"
}
]
}
}

View file

@ -0,0 +1,26 @@
{
"realm": "servlet-authz",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url" : "http://localhost:8180/auth",
"ssl-required" : "external",
"resource" : "servlet-authz-app",
"public-client" : false,
"credentials": {
"secret": "secret"
},
"policy-enforcer": {
"on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp",
"path-cache": {
"lifespan": 5000,
"max-entries": 1000
},
"paths": [
{
"name": "Premium Resource",
"path": "/protected/premium/pep-disabled.jsp",
"enforcement-mode": "DISABLED"
}
]
}
}

View file

@ -0,0 +1,86 @@
/*
* 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.testsuite.adapter.example.authorization;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public abstract class AbstractServletCacheDisabledAdapterTest extends AbstractServletAuthzFunctionalAdapterTest {
@Deployment(name = RESOURCE_SERVER_ID, managed = false)
public static WebArchive deployment() throws IOException {
return exampleDeployment(RESOURCE_SERVER_ID)
.addAsWebInfResource(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/keycloak-cache-disabled-authz-service.json"), "keycloak.json");
}
@Test
public void testCreateNewResource() {
performTests(() -> {
login("alice", "alice");
assertFalse(wasDenied());
this.driver.navigate().to(getResourceServerUrl() + "/new-resource");
assertFalse(wasDenied());
ResourceRepresentation resource = new ResourceRepresentation();
resource.setName("New Resource");
resource.setUri("/new-resource");
getAuthorizationResource().resources().create(resource);
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName(resource.getName() + " Permission");
permission.addResource(resource.getName());
permission.addPolicy("Deny Policy");
permission = getAuthorizationResource().permissions().resource().create(permission).readEntity(ResourcePermissionRepresentation.class);
login("alice", "alice");
assertFalse(wasDenied());
this.driver.navigate().to(getResourceServerUrl() + "/new-resource");
assertTrue(wasDenied());
permission = getAuthorizationResource().permissions().resource().findById(permission.getId()).toRepresentation();
permission.removePolicy("Deny Policy");
permission.addPolicy("Any User Policy");
getAuthorizationResource().permissions().resource().findById(permission.getId()).update(permission);
login("alice", "alice");
assertFalse(wasDenied());
this.driver.navigate().to(getResourceServerUrl() + "/new-resource");
assertFalse(wasDenied());
});
}
}

View file

@ -0,0 +1,81 @@
/*
* 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.testsuite.adapter.example.authorization;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public abstract class AbstractServletCacheLifespanAdapterTest extends AbstractServletAuthzFunctionalAdapterTest {
@Deployment(name = RESOURCE_SERVER_ID, managed = false)
public static WebArchive deployment() throws IOException {
return exampleDeployment(RESOURCE_SERVER_ID)
.addAsWebInfResource(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/keycloak-cache-lifespan-authz-service.json"), "keycloak.json");
}
@Test
public void testCreateNewResourceWaitExpiration() {
performTests(() -> {
login("alice", "alice");
assertFalse(wasDenied());
this.driver.navigate().to(getResourceServerUrl() + "/new-resource");
assertFalse(wasDenied());
ResourceRepresentation resource = new ResourceRepresentation();
resource.setName("New Resource");
resource.setUri("/new-resource");
getAuthorizationResource().resources().create(resource);
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName(resource.getName() + " Permission");
permission.addResource(resource.getName());
permission.addPolicy("Deny Policy");
permission = getAuthorizationResource().permissions().resource().create(permission).readEntity(ResourcePermissionRepresentation.class);
login("alice", "alice");
assertFalse(wasDenied());
this.driver.navigate().to(getResourceServerUrl() + "/new-resource");
assertFalse(wasDenied());
Thread.sleep(5000);
login("alice", "alice");
assertFalse(wasDenied());
this.driver.navigate().to(getResourceServerUrl() + "/new-resource");
assertTrue(wasDenied());
});
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.testsuite.adapter.example.authorization;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
/**
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@RunAsClient
@AppServerContainer("app-server-wildfly")
//@AdapterLibsLocationProperty("adapter.libs.wildfly")
public class WildflyServletCacheDisabledAdapterTest extends AbstractServletCacheDisabledAdapterTest {
}

View file

@ -0,0 +1,31 @@
/*
* 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.testsuite.adapter.example.authorization;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
/**
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@RunAsClient
@AppServerContainer("app-server-wildfly")
//@AdapterLibsLocationProperty("adapter.libs.wildfly")
public class WildflyServletCacheLifespanAdapterTest extends AbstractServletCacheLifespanAdapterTest {
}