keycloak-aplcache

[KEYCLOAK-5726] - Support define enforcement mode for scopes

10/20/2017 8:51:19 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 f3127be..d3ef9cd 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
@@ -33,6 +33,7 @@ import org.keycloak.authorization.client.ClientAuthorizationContext;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
 import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
 import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
 import org.keycloak.representations.idm.authorization.Permission;
 
@@ -96,9 +97,9 @@ public abstract class AbstractPolicyEnforcer {
                     return createEmptyAuthorizationContext(true);
                 }
 
-                Set<String> requiredScopes = getRequiredScopes(pathConfig, request);
+                MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
 
-                if (isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
+                if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
                     try {
                         return createAuthorizationContext(accessToken, pathConfig);
                     } catch (Exception e) {
@@ -108,7 +109,7 @@ public abstract class AbstractPolicyEnforcer {
 
                 LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
 
-                if (!challenge(pathConfig, requiredScopes, httpFacade)) {
+                if (!challenge(pathConfig, methodConfig, httpFacade)) {
                     LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
                     handleAccessDenied(httpFacade);
                 }
@@ -118,9 +119,9 @@ public abstract class AbstractPolicyEnforcer {
         return createEmptyAuthorizationContext(false);
     }
 
-    protected abstract boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade);
+    protected abstract boolean challenge(PathConfig pathConfig, MethodConfig methodConfig, OIDCHttpFacade facade);
 
-    protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
+    protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
         Request request = httpFacade.getRequest();
         PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
 
@@ -146,7 +147,7 @@ public abstract class AbstractPolicyEnforcer {
                         continue;
                     }
 
-                    if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
+                    if (hasResourceScopePermission(methodConfig, permission)) {
                         LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
                         if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
                             this.paths.remove(actualPathConfig);
@@ -155,7 +156,7 @@ public abstract class AbstractPolicyEnforcer {
                     }
                 }
             } else {
-                if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
+                if (hasResourceScopePermission(methodConfig, permission)) {
                     hasPermission = true;
                     return true;
                 }
@@ -166,7 +167,7 @@ public abstract class AbstractPolicyEnforcer {
             return true;
         }
 
-        LOGGER.debugf("Authorization FAILED for path [%s]. No enough permissions [%s].", actualPathConfig, permissions);
+        LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, permissions);
 
         return false;
     }
@@ -186,9 +187,28 @@ public abstract class AbstractPolicyEnforcer {
         return false;
     }
 
-    private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
+    private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
         Set<String> allowedScopes = permission.getScopes();
-        return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
+
+        if (allowedScopes.isEmpty()) {
+            return true;
+        }
+
+        PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode();
+
+        if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) {
+            return allowedScopes.containsAll(methodConfig.getScopes());
+        }
+
+        if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) {
+            for (String requiredScope : methodConfig.getScopes()) {
+                if (allowedScopes.contains(requiredScope)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
     }
 
     protected AuthzClient getAuthzClient() {
@@ -236,20 +256,22 @@ public abstract class AbstractPolicyEnforcer {
         return request.getRelativePath();
     }
 
-    private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
-        Set<String> requiredScopes = new HashSet<>();
-
-        requiredScopes.addAll(pathConfig.getScopes());
-
+    private MethodConfig getRequiredScopes(PathConfig pathConfig, Request request) {
         String method = request.getMethod();
 
-        for (PolicyEnforcerConfig.MethodConfig methodConfig : pathConfig.getMethods()) {
+        for (MethodConfig methodConfig : pathConfig.getMethods()) {
             if (methodConfig.getMethod().equals(method)) {
-                requiredScopes.addAll(methodConfig.getScopes());
+                return methodConfig;
             }
         }
 
-        return requiredScopes;
+        MethodConfig methodConfig = new MethodConfig();
+
+        methodConfig.setMethod(request.getMethod());
+        methodConfig.setScopes(pathConfig.getScopes());
+        methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY);
+
+        return methodConfig;
     }
 
     private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
index f2555d4..172c745 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
@@ -17,7 +17,7 @@
  */
 package org.keycloak.adapters.authorization;
 
-import java.util.Set;
+import java.util.HashSet;
 
 import org.jboss.logging.Logger;
 import org.keycloak.adapters.OIDCHttpFacade;
@@ -26,6 +26,7 @@ import org.keycloak.authorization.client.AuthzClient;
 import org.keycloak.authorization.client.representation.PermissionRequest;
 import org.keycloak.authorization.client.resource.PermissionResource;
 import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
 import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
 
 /**
@@ -40,9 +41,9 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
     }
 
     @Override
-    protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
+    protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
         if (getEnforcerConfig().getUserManagedAccess() != null) {
-            challengeUmaAuthentication(pathConfig, requiredScopes, facade);
+            challengeUmaAuthentication(pathConfig, methodConfig, facade);
         } else {
             challengeEntitlementAuthentication(facade);
         }
@@ -61,10 +62,10 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
         }
     }
 
-    private void challengeUmaAuthentication(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
+    private void challengeUmaAuthentication(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
         HttpFacade.Response response = facade.getResponse();
         AuthzClient authzClient = getAuthzClient();
-        String ticket = getPermissionTicket(pathConfig, requiredScopes, authzClient);
+        String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
         String clientId = authzClient.getConfiguration().getResource();
         String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
         response.setStatus(401);
@@ -74,12 +75,12 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
         }
     }
 
-    private String getPermissionTicket(PathConfig pathConfig, Set<String> requiredScopes, AuthzClient authzClient) {
+    private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient) {
         ProtectionResource protection = authzClient.protection();
         PermissionResource permission = protection.permission();
         PermissionRequest permissionRequest = new PermissionRequest();
         permissionRequest.setResourceSetId(pathConfig.getId());
-        permissionRequest.setScopes(requiredScopes);
+        permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
         return permission.forResource(permissionRequest).getTicket();
     }
 }
\ No newline at end of file
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
index 0dbddd4..65fdc1e 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
@@ -35,6 +35,7 @@ import org.keycloak.authorization.client.representation.EntitlementResponse;
 import org.keycloak.authorization.client.representation.PermissionRequest;
 import org.keycloak.authorization.client.representation.PermissionResponse;
 import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
 import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
 import org.keycloak.representations.idm.authorization.Permission;
 
@@ -50,14 +51,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
     }
 
     @Override
-    protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
+    protected boolean isAuthorized(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
         AccessToken original = accessToken;
 
-        if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
+        if (super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
             return true;
         }
 
-        accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
+        accessToken = requestAuthorizationToken(pathConfig, methodConfig, httpFacade);
 
         if (accessToken == null) {
             return false;
@@ -78,11 +79,11 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
 
         original.setAuthorization(authorization);
 
-        return super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade);
+        return super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade);
     }
 
     @Override
-    protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
+    protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
         handleAccessDenied(facade);
         return true;
     }
@@ -100,7 +101,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
         }
     }
 
-    private AccessToken requestAuthorizationToken(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade httpFacade) {
+    private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
         try {
             String accessToken = httpFacade.getSecurityContext().getTokenString();
             AuthzClient authzClient = getAuthzClient();
@@ -111,7 +112,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
                 PermissionRequest permissionRequest = new PermissionRequest();
 
                 permissionRequest.setResourceSetId(pathConfig.getId());
-                permissionRequest.setScopes(requiredScopes);
+                permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
 
                 PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest);
                 AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket());
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 a495cad..67c3c59 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
@@ -220,6 +220,9 @@ public class PolicyEnforcerConfig {
         private String method;
         private List<String> scopes = Collections.emptyList();
 
+        @JsonProperty("scopes-enforcement-mode")
+        private ScopeEnforcementMode scopesEnforcementMode = ScopeEnforcementMode.ALL;
+
         public String getMethod() {
             return method;
         }
@@ -235,6 +238,14 @@ public class PolicyEnforcerConfig {
         public void setScopes(List<String> scopes) {
             this.scopes = scopes;
         }
+
+        public void setScopesEnforcementMode(ScopeEnforcementMode scopesEnforcementMode) {
+            this.scopesEnforcementMode = scopesEnforcementMode;
+        }
+
+        public ScopeEnforcementMode getScopesEnforcementMode() {
+            return scopesEnforcementMode;
+        }
     }
 
     public enum EnforcementMode {
@@ -243,6 +254,11 @@ public class PolicyEnforcerConfig {
         DISABLED
     }
 
+    public enum ScopeEnforcementMode {
+        ALL,
+        ANY
+    }
+
     public static class UmaProtocolConfig {
 
     }
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
index bb21e50..7ec7e02 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
@@ -108,7 +108,7 @@
       "redirectUris": [
         "/photoz-html5-client/*"
       ],
-      "webOrigins": ["*"]
+      "webOrigins": ["http://localhost:8280"]
     },
     {
       "clientId": "photoz-restful-api",
@@ -118,7 +118,7 @@
       "redirectUris": [
         "/photoz-restful-api/*"
       ],
-      "webOrigins" : ["*"],
+      "webOrigins" : ["http://localhost:8280"],
       "clientAuthenticatorType": "client-jwt",
       "attributes" : {
         "jwt.credential.certificate" : "MIICqTCCAZECBgFT0Ngs/DANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zZWN1cmUtcG9ydGFsMB4XDTE2MDQwMTA4MDA0MVoXDTI2MDQwMTA4MDIyMVowGDEWMBQGA1UEAwwNc2VjdXJlLXBvcnRhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJa4GixpmzP511AmI0eLPLORyJwXS8908MUvdG3hmh8jMOIhe28XjIFeZSY09vFxh22F2SUMjxU/B2Hw4PDJUkebuNR7rXhOIYCJAo6eEZzjSBY/wngFtfm74zJ/eLCobBtDvIld7jobdHTfE1Oz9+GzvtG0k7cm7ubrLT0J4I1UsFZj3b//3wa+O0vNaTwHC1Jz/m59VbtXqyO4xEzIdl416cnGCmEmk5qd5h1de2UoLi/CTad8HftIJhzN1qhlySzW/9Ha70aYlDH2hiibDsXDTrNaMdaaLik7I8Rv/nIbggysG863PKZo8wknDe62QctH5VYSSktiy4gjSJkGh7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZnnx+AHQ8txugGcFK8gWjildDgk+v31fBHBDvmLQaSzsUaIOJaK4wnlwUI+VfR46HmBXhjlDCobFLUptd+kz0G7xapcIn3b5jLrySUUD7L+LAp1vNOQU4mKhTGS3IEvNB73D3GH9rQ+M3KEcoN3f99fNKqKsUdxbmZqGf4VOQ57PUfLBw4PJJGlROPosBc7ivPRyeYnKekhoCTynq30BAD1FA1BA8ppcY4ZVGADPTAgMJxpglpFY9LiqCwdLAGW1ttnsyIJ7DpT+kybhhk7c+MU7gyQdv8xPnMR0bSCB9hndowgBn5oZ393aMscwMNCzwJ0aWBs1sUyn3X0RIsu9Jg=="
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
index 43ebde4..0a31003 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
@@ -46,6 +46,18 @@
           "name": "urn:servlet-authz:page:main:actionForPremiumUser"
         }
       ]
+    },
+    {
+      "name": "Resource A",
+      "uri": "/protected/scopes.jsp",
+      "scopes": [
+        {
+          "name": "read"
+        },
+        {
+          "name": "write"
+        }
+      ]
     }
   ],
   "policies": [
@@ -142,6 +154,37 @@
         "scopes": "[\"urn:servlet-authz:page:main:actionForPremiumUser\"]",
         "applyPolicies": "[\"Only Premium User Policy\"]"
       }
+    },
+    {
+      "name": "Deny Policy",
+      "type": "js",
+      "logic": "POSITIVE",
+      "decisionStrategy": "UNANIMOUS",
+      "config": {
+        "code": "// by default, grants any permission associated with this policy\n$evaluation.deny();"
+      }
+    },
+    {
+      "name": "Resource A Read Permission",
+      "type": "scope",
+      "logic": "POSITIVE",
+      "decisionStrategy": "UNANIMOUS",
+      "config": {
+        "resources": "[\"Resource A\"]",
+        "scopes": "[\"read\"]",
+        "applyPolicies": "[\"Any User Policy\"]"
+      }
+    },
+    {
+      "name": "Resource A Write Permission",
+      "type": "scope",
+      "logic": "POSITIVE",
+      "decisionStrategy": "UNANIMOUS",
+      "config": {
+        "resources": "[\"Resource A\"]",
+        "scopes": "[\"write\"]",
+        "applyPolicies": "[\"Deny Policy\"]"
+      }
     }
   ]
 }
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/scopes.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/scopes.jsp
new file mode 100644
index 0000000..405921b
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/scopes.jsp
@@ -0,0 +1 @@
+Granted
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
index a46f8f1..63852d0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
@@ -307,4 +307,14 @@ public abstract class AbstractServletAuthzFunctionalAdapterTest extends Abstract
             assertFalse(wasDenied());
         });
     }
+
+    @Test
+    public void testAccessResourceWithAnyScope() throws Exception {
+        performTests(() -> {
+            login("jdoe", "jdoe");
+            driver.navigate().to(getResourceServerUrl() + "/protected/scopes.jsp");
+            WaitUtils.waitForPageToLoad();
+            assertTrue(hasText("Granted"));
+        });
+    }
 }