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 d90a4fd7fd..9efa614573 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 @@ -49,7 +49,6 @@ class PathMatcher { PathConfig matchingAnyPath = null; PathConfig matchingAnySuffixPath = null; - PathConfig matchingPath = null; for (PathConfig entry : paths.values()) { String expectedUri = entry.getPath(); @@ -132,58 +131,79 @@ class PathMatcher { return targetUri.startsWith(expectedUri.substring(0, expectedUri.length() - 2)); } + String suffix = "/*."; + int suffixIndex = expectedUri.indexOf(suffix); + + if (suffixIndex != -1) { + return targetUri.endsWith(expectedUri.substring(suffixIndex + suffix.length() - 1)); + } + return false; } public String buildUriFromTemplate(String expectedUri, String targetUri) { int patternStartIndex = expectedUri.indexOf("{"); - if (patternStartIndex >= targetUri.length()) { + if (patternStartIndex == -1 || patternStartIndex >= targetUri.length()) { + return null; + } + + if (expectedUri.split("/").length > targetUri.split("/").length) { return null; } char[] expectedUriChars = expectedUri.toCharArray(); char[] matchingUri = Arrays.copyOfRange(expectedUriChars, 0, patternStartIndex); + int matchingUriLastIndex = matchingUri.length; + String targetUriParams = targetUri.substring(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; + matchingUri = Arrays.copyOf(matchingUri, targetUri.length()); + int paramIndex = 0; - 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) { + for (int i = patternStartIndex; i < expectedUriChars.length; i++) { + if (matchingUriLastIndex >= matchingUri.length) { break; } - if ((targetPatternStartIndex == 0 || targetPatternStartIndex == targetUri.length()) && parameterStartIndex != -1) { - return null; + char c = expectedUriChars[i]; + + if (c == '{' || c == '*') { + String[] params = targetUriParams.split("/"); + + for (int k = paramIndex; k <= (c == '*' ? params.length : paramIndex); k++) { + if (k == params.length) { + break; + } + + int paramLength = params[k].length(); + + if (matchingUriLastIndex + paramLength > matchingUri.length) { + return null; + } + + for (int j = 0; j < paramLength; j++) { + matchingUri[matchingUriLastIndex++] = params[k].charAt(j); + } + + if (c == '*' && matchingUriLastIndex < matchingUri.length) { + matchingUri[matchingUriLastIndex++] = '/'; + } + } + + i = expectedUri.indexOf('}', i); + } else { + if (c == '/') { + paramIndex++; + } + matchingUri[matchingUriLastIndex++] = c; } } + if (matchingUri[matchingUri.length - 1] == '\u0000') { + return null; + } + return String.valueOf(matchingUri); } 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 8a6a0a5bb1..679a33c867 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 @@ -220,7 +220,14 @@ public class PolicyEnforcer { pathConfig.setId(resourceDescription.getId()); pathConfig.setName(resourceDescription.getName()); - pathConfig.setPath(resourceDescription.getUri()); + + String uri = resourceDescription.getUri(); + + if (uri == null || "".equals(uri.trim())) { + throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "]."); + } + + pathConfig.setPath(uri); List scopeNames = new ArrayList<>(); diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java index a1bc1790bd..dd94537645 100644 --- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java @@ -196,10 +196,12 @@ public class PolicyEnforcerConfig { '}'; } + @JsonIgnore public boolean hasPattern() { return getPath().indexOf("{") != -1; } + @JsonIgnore public boolean isInstance() { return this.parentConfig != null; } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java index d4dd0ed376..c833f01d40 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java @@ -17,13 +17,16 @@ package org.keycloak.admin.client.resource; import javax.ws.rs.Consumes; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; /** @@ -39,4 +42,9 @@ public interface ResourcePermissionsResource { @Path("{id}") ResourcePermissionResource findById(@PathParam("id") String id); + @Path("/search") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + ResourcePermissionRepresentation findByName(@QueryParam("name") String name); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RolePoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RolePoliciesResource.java index 9ed6226f29..f9f1f98bbc 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RolePoliciesResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RolePoliciesResource.java @@ -17,13 +17,16 @@ package org.keycloak.admin.client.resource; import javax.ws.rs.Consumes; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; /** @@ -39,4 +42,9 @@ public interface RolePoliciesResource { @Path("{id}") RolePolicyResource findById(@PathParam("id") String id); + @Path("/search") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + RolePolicyRepresentation findByName(@QueryParam("name") String name); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java index c92b132f10..ab0dd5c2fb 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java @@ -17,13 +17,16 @@ package org.keycloak.admin.client.resource; import javax.ws.rs.Consumes; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; /** @@ -38,4 +41,10 @@ public interface ScopePermissionsResource { @Path("{id}") ScopePermissionResource findById(@PathParam("id") String id); + + @Path("/search") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + ScopePermissionRepresentation findByName(@QueryParam("name") String name); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserPoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserPoliciesResource.java index c1769d70dc..702995f50a 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserPoliciesResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserPoliciesResource.java @@ -17,13 +17,16 @@ package org.keycloak.admin.client.resource; import javax.ws.rs.Consumes; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; /** @@ -39,4 +42,9 @@ public interface UserPoliciesResource { @Path("{id}") UserPolicyResource findById(@PathParam("id") String id); + @Path("/search") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + UserPolicyRepresentation findByName(@QueryParam("name") String name); } diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java index 5f7520fc44..c89467466d 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java @@ -79,6 +79,7 @@ public class KeycloakOnUndertow implements DeployableContainerphotoz hello-world-authz-service servlet-authz + servlet-policy-enforcer servlets app-profile-jee cors diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml new file mode 100755 index 0000000000..d6eb83364f --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/pom.xml @@ -0,0 +1,53 @@ + + + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-test-apps + 3.1.0.CR1-SNAPSHOT + + + servlet-policy-enforcer + war + + Keycloak Authz: Simple Servlet App with Policy Enforcer + + + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + + diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json new file mode 100644 index 0000000000..cef6f0057b --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json @@ -0,0 +1,265 @@ +{ + "realm": "servlet-policy-enforcer-authz", + "enabled": true, + "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "requiredCredentials": [ + "password" + ], + "users": [ + { + "username": "alice", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "alice" + } + ], + "realmRoles": [ + "uma_authorization" + ] + }, + { + "username": "jdoe", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "jdoe" + } + ], + "realmRoles": [ + "uma_authorization" + ] + }, + { + "username": "service-account-servlet-policy-enforcer", + "enabled": true, + "serviceAccountClientId": "servlet-policy-enforcer", + "clientRoles": { + "servlet-policy-enforcer": [ + "uma_protection" + ] + } + } + ], + "clients": [ + { + "clientId": "servlet-policy-enforcer", + "secret": "secret", + "authorizationServicesEnabled": true, + "enabled": true, + "redirectUris": [ + "/servlet-policy-enforcer/*" + ], + "baseUrl": "/servlet-policy-enforcer", + "adminUrl": "/servlet-policy-enforcer", + "directAccessGrantsEnabled": true, + "authorizationSettings": { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Welcome Resource", + "uri": "", + "typedScopes": [] + }, + { + "name": "Pattern 1", + "uri": "", + "typedScopes": [] + }, + { + "name": "Pattern 2", + "uri": "/resource/resource-a", + "typedScopes": [] + }, + { + "name": "Pattern 3", + "uri": "/resource/resource-b/test", + "typedScopes": [] + }, + { + "name": "Pattern 4", + "uri": "/resource-c", + "typedScopes": [] + }, + { + "name": "Pattern 5", + "uri": "/resource/d/resource-d", + "typedScopes": [] + }, + { + "name": "Pattern 6", + "uri": "", + "typedScopes": [] + }, + { + "name": "Pattern 7", + "uri": "", + "typedScopes": [] + }, + { + "name": "Pattern 8", + "typedScopes": [] + }, + { + "name": "Pattern 9", + "typedScopes": [] + }, + { + "name": "Pattern 10", + "typedScopes": [] + }, + { + "name": "Pattern 11", + "typedScopes": [] + } + ], + "policies": [ + { + "name": "Default Policy", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n", + "applyPolicies": "[]" + } + }, + { + "name": "Deny Policy", + "type": "js", + "logic": "NEGATIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "code": "$evaluation.grant();" + } + }, + { + "name": "Pattern 3 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"Pattern 3\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 2 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"Pattern 2\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 4 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Pattern 4\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 5 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Pattern 5\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 7 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"Pattern 7\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 8 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Pattern 8\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 9 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Pattern 9\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 6 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"Pattern 6\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Welcome Resource Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Welcome Resource\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 1 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Pattern 1\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 10 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Pattern 10\"]", + "applyPolicies": "[\"Default Policy\"]" + } + }, + { + "name": "Pattern 11 Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Pattern 11\"]", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [] + } + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 0000000000..d8742d3c4b --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,62 @@ +{ + "realm": "servlet-policy-enforcer-authz", + "auth-server-url": "http://localhost:8180/auth", + "ssl-required": "external", + "resource": "servlet-policy-enforcer", + "credentials": { + "secret": "secret" + }, + "policy-enforcer": { + "on-deny-redirect-to": "/servlet-policy-enforcer/denied.jsp", + "paths": [ + { + "name": "Welcome Resource", + "path": "/index.jsp" + }, + { + "name": "Pattern 1", + "path": "/resource/{pattern}/{sub-resource}" + }, + { + "name": "Pattern 2", + "path": "/{pattern}/resource-a" + }, + { + "name": "Pattern 3", + "path": "/{pattern}/resource-b" + }, + { + "name": "Pattern 4", + "path": "/resource-c" + }, + { + "name": "Pattern 5", + "path": "/resource/{pattern}/resource-d" + }, + { + "name": "Pattern 6", + "path": "/resource/{pattern}" + }, + { + "name": "Pattern 7", + "path": "/resource/{pattern}/f/{resource}" + }, + { + "name": "Pattern 8", + "path": "/resource" + }, + { + "name": "Pattern 9", + "path": "/file/*.suffix" + }, + { + "name": "Pattern 10", + "path": "/resource/{pattern}/i/{resource}/*" + }, + { + "name": "Pattern 11", + "path": "/api/{version}/{resource}" + } + ] + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..3916450d41 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,45 @@ + + + + + + servlet-policy-enforcer + + + + All Resources + /* + + + uma_authorization + + + + + KEYCLOAK + servlet-policy-enforcer + + + + uma_authorization + + diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/denied.jsp b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/denied.jsp new file mode 100644 index 0000000000..ef8b4a6864 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/denied.jsp @@ -0,0 +1,2 @@ +<%@include file="logout-include.jsp"%> +

You can not access this resource

\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/index.jsp b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/index.jsp new file mode 100644 index 0000000000..cf0e035222 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/index.jsp @@ -0,0 +1,2 @@ +<%@include file="logout-include.jsp"%> +

Welcome

\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/logout-include.jsp b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/logout-include.jsp new file mode 100644 index 0000000000..3c55fd98c4 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/logout-include.jsp @@ -0,0 +1,11 @@ +<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %> +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> +<% + String scheme = request.getScheme(); + String host = request.getServerName(); + int port = request.getServerPort(); + String contextPath = request.getContextPath(); + String redirectUri = scheme + "://" + host + ":" + port + contextPath; +%> +

Click here ">Sign Out

\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml index ff9f05daf3..e3200c5a46 100755 --- a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml +++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml @@ -51,6 +51,14 @@ + + + + + + + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java new file mode 100644 index 0000000000..aaeee4f566 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java @@ -0,0 +1,447 @@ +/* + * 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.testsuite.adapter.example.authorization; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.IOUtil.loadRealm; +import static org.keycloak.testsuite.util.WaitUtils.pause; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import org.jboss.arquillian.container.test.api.Deployer; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.ResourcePermissionsResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.testsuite.ProfileAssume; +import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; +import org.keycloak.testsuite.util.WaitUtils; +import org.openqa.selenium.By; + +/** + * @author Pedro Igor + */ +public abstract class AbstractServletPolicyEnforcerTest extends AbstractExampleAdapterTest { + + protected static final String REALM_NAME = "servlet-policy-enforcer-authz"; + protected static final String RESOURCE_SERVER_ID = "servlet-policy-enforcer"; + + @BeforeClass + public static void enabled() { ProfileAssume.assumePreview(); } + + @ArquillianResource + private Deployer deployer; + + @Override + public void addAdapterTestRealms(List testRealms) { + testRealms.add( + loadRealm(new File(TEST_APPS_HOME_DIR + "/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json"))); + } + + @Deployment(name = RESOURCE_SERVER_ID, managed = false) + public static WebArchive deployment() throws IOException { + return exampleDeployment(RESOURCE_SERVER_ID); + } + + @Test + public void testPattern1() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/resource/a/b"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 1 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/resource/a/b"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 1 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/resource/a/b"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern2() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/a/resource-a"); + assertFalse(wasDenied()); + navigateTo("/b/resource-a"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 2 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/a/resource-a"); + assertTrue(wasDenied()); + navigateTo("/b/resource-a"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 2 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/b/resource-a"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern3() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/a/resource-b"); + assertFalse(wasDenied()); + navigateTo("/b/resource-b"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 3 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/a/resource-b"); + assertTrue(wasDenied()); + navigateTo("/b/resource-b"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 3 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/b/resource-b"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 2 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/b/resource-a"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 3 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/a/resource-b"); + assertTrue(wasDenied()); + navigateTo("/b/resource-a"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern4() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/resource-c"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 4 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/resource-c"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 4 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/resource-c"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern5() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/resource/a/resource-d"); + assertFalse(wasDenied()); + navigateTo("/resource/b/resource-d"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 5 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/resource/a/resource-d"); + assertTrue(wasDenied()); + navigateTo("/resource/b/resource-d"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 5 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/resource/b/resource-d"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern6() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/resource/a"); + assertFalse(wasDenied()); + navigateTo("/resource/b"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 6 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/resource/a"); + assertTrue(wasDenied()); + navigateTo("/resource/b"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 6 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/resource/b"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern7() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/resource/a/f/b"); + assertFalse(wasDenied()); + navigateTo("/resource/c/f/d"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 7 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/resource/a/f/b"); + assertTrue(wasDenied()); + navigateTo("/resource/c/f/d"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 7 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/resource/c/f/d"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern8() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/resource"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 8 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/resource"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 8 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/resource"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern9() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/file/*.suffix"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 9 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/file/*.suffix"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 9 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/file/*.suffix"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern10() throws Exception { + performTests(() -> { + login("alice", "alice"); + + navigateTo("/resource/a/i/b/c/d/e"); + navigateTo("/resource/a/i/b/c/"); + assertFalse(wasDenied()); + + updatePermissionPolicies("Pattern 10 Permission", "Deny Policy"); + login("alice", "alice"); + navigateTo("/resource/a/i/b/c/d/e"); + navigateTo("/resource/a/i/b/c/d"); + assertTrue(wasDenied()); + + updatePermissionPolicies("Pattern 10 Permission", "Default Policy"); + login("alice", "alice"); + navigateTo("/resource/a/i/b/c/d"); + assertFalse(wasDenied()); + }); + } + + @Test + public void testPattern11UsingResourceInstancePermission() throws Exception { + performTests(() -> { + login("alice", "alice"); + navigateTo("/api/v1/resource-a"); + assertFalse(wasDenied()); + navigateTo("/api/v1/resource-b"); + assertFalse(wasDenied()); + + ResourceRepresentation resource = new ResourceRepresentation("/api/v1/resource-c"); + + resource.setUri(resource.getName()); + + getAuthorizationResource().resources().create(resource); + + createResourcePermission(resource.getName() + " permission", resource.getName(), "Default Policy"); + + login("alice", "alice"); + navigateTo(resource.getUri()); + assertFalse(wasDenied()); + + updatePermissionPolicies(resource.getName() + " permission", "Deny Policy"); + + login("alice", "alice"); + navigateTo(resource.getUri()); + assertTrue(wasDenied()); + + updatePermissionPolicies(resource.getName() + " permission", "Default Policy"); + + login("alice", "alice"); + navigateTo(resource.getUri()); + assertFalse(wasDenied()); + + navigateTo("/api/v1"); + assertTrue(wasDenied()); + navigateTo("/api/v1/"); + assertTrue(wasDenied()); + navigateTo("/api"); + assertTrue(wasDenied()); + navigateTo("/api/"); + assertTrue(wasDenied()); + }); + } + + private void navigateTo(String path) { + this.driver.navigate().to(getResourceServerUrl() + path); + } + + private void performTests(ExceptionRunnable assertion) { + performTests(() -> {}, assertion); + } + + private void performTests(ExceptionRunnable beforeDeploy, ExceptionRunnable assertion) { + try { + beforeDeploy.run(); + deployer.deploy(RESOURCE_SERVER_ID); + assertion.run(); + } catch (FileNotFoundException cause) { + throw new RuntimeException("Failed to import authorization settings", cause); + } catch (Exception cause) { + throw new RuntimeException("Error while executing tests", cause); + } finally { + deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + private AuthorizationResource getAuthorizationResource() { + return getClientResource(RESOURCE_SERVER_ID).authorization(); + } + + private ClientResource getClientResource(String clientId) { + ClientsResource clients = this.realmsResouce().realm(REALM_NAME).clients(); + ClientRepresentation resourceServer = clients.findByClientId(clientId).get(0); + return clients.get(resourceServer.getId()); + } + + private void logOut() { + navigateTo(); + By by = By.xpath("//a[text() = 'Sign Out']"); + WaitUtils.waitUntilElement(by); + this.driver.findElement(by).click(); + pause(500); + } + + private void login(String username, String password) { + try { + navigateTo(); + if (this.driver.getCurrentUrl().startsWith(getResourceServerUrl().toString())) { + logOut(); + navigateTo(); + } + this.loginPage.form().login(username, password); + navigateTo(); + assertFalse(wasDenied()); + } catch (Exception cause) { + throw new RuntimeException("Login failed", cause); + } + } + + private void navigateTo() { + this.driver.navigate().to(getResourceServerUrl()); + WaitUtils.waitUntilElement(By.xpath("//p[text() = 'Welcome']")); + } + + private boolean wasDenied() { + return this.driver.getPageSource().contains("You can not access this resource"); + } + + private URL getResourceServerUrl() { + try { + return new URL(this.appServerContextRootPage + "/" + RESOURCE_SERVER_ID); + } catch (MalformedURLException e) { + throw new RuntimeException("Could not obtain resource server url.", e); + } + } + + private void updatePermissionPolicies(String permissionName, String... policyNames) { + ResourcePermissionsResource permissions = getAuthorizationResource().permissions().resource(); + ResourcePermissionRepresentation permission = permissions.findByName(permissionName); + + permission.addPolicy(policyNames); + + permissions.findById(permission.getId()).update(permission); + } + + private void createResourcePermission(String name, String resourceName, String... policyNames) { + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName(name); + permission.addResource(resourceName); + permission.addPolicy(policyNames); + + getAuthorizationResource().permissions().resource().create(permission); + } + + private interface ExceptionRunnable { + void run() throws Exception; + } +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletPolicyEnforcerAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletPolicyEnforcerAdapterTest.java new file mode 100644 index 0000000000..8ad7eb1621 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletPolicyEnforcerAdapterTest.java @@ -0,0 +1,30 @@ +/* + * 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.testsuite.adapter.example.authorization; + +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * @author Pedro Igor + */ +@RunAsClient +@AppServerContainer("app-server-wildfly") +//@AdapterLibsLocationProperty("adapter.libs.wildfly") +public class WildflyServletPolicyEnforcerAdapterTest extends AbstractServletPolicyEnforcerTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index 9e13566c18..75a68ead6f 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -350,6 +350,12 @@ ${project.version} war + + org.keycloak.testsuite + servlet-policy-enforcer + ${project.version} + war + org.keycloak.testsuite integration-arquillian-test-apps-cors-angular-product