keycloak-uncached

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
index 764ae02..8a34e98 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 
 import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
 
@@ -40,6 +41,7 @@ public class AuthorizationRequest {
     private String audience;
     private String accessToken;
     private boolean submitRequest;
+    private Map<String, Object> claims;
 
     public AuthorizationRequest(String ticket) {
         this.ticket = ticket;
@@ -129,6 +131,14 @@ public class AuthorizationRequest {
         return accessToken;
     }
 
+    public Map<String, Object> getClaims() {
+        return claims;
+    }
+
+    public void setClaims(Map<String, Object> claims) {
+        this.claims = claims;
+    }
+
     public void addPermission(String resourceId, List<String> scopes) {
         addPermission(resourceId, scopes.toArray(new String[scopes.size()]));
     }
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
index 5830e16..bd2a94b 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
@@ -18,10 +18,15 @@
 package org.keycloak.representations.idm.authorization;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.keycloak.json.StringListMapDeserializer;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -32,6 +37,9 @@ public class PermissionRequest {
     private Set<String> scopes;
     private String resourceServerId;
 
+    @JsonDeserialize(using = StringListMapDeserializer.class)
+    private Map<String, List<String>> claims;
+
     public PermissionRequest(String resourceId, String... scopes) {
         this.resourceId = resourceId;
         if (scopes != null) {
@@ -69,4 +77,28 @@ public class PermissionRequest {
     public String getResourceServerId() {
         return resourceServerId;
     }
+
+    public Map<String, List<String>> getClaims() {
+        return claims;
+    }
+
+    public void setClaims(Map<String, List<String>> claims) {
+        this.claims = claims;
+    }
+
+    public void setClaim(String name, String... value) {
+        if (claims == null) {
+            claims = new HashMap<>();
+        }
+
+        claims.put(name, Arrays.asList(value));
+    }
+
+    public void addScope(String... name) {
+        if (scopes == null) {
+            scopes = new HashSet<>();
+        }
+
+        scopes.addAll(Arrays.asList(name));
+    }
 }
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
index ff4a927..d370cf7 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
@@ -18,10 +18,13 @@ package org.keycloak.representations.idm.authorization;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import org.keycloak.TokenIdGenerator;
+import org.keycloak.json.StringListMapDeserializer;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.JsonWebToken;
 
@@ -32,6 +35,9 @@ public class PermissionTicketToken extends JsonWebToken {
 
     private final List<ResourcePermission> resources;
 
+    @JsonDeserialize(using = StringListMapDeserializer.class)
+    private Map<String, List<String>> claims;
+
     public PermissionTicketToken() {
         this(new ArrayList<ResourcePermission>());
     }
@@ -59,6 +65,10 @@ public class PermissionTicketToken extends JsonWebToken {
         return this.resources;
     }
 
+    public Map<String, List<String>> getClaims() {
+        return claims;
+    }
+
     public static class ResourcePermission {
 
         @JsonProperty("id")
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
index 0719bab..8f33fcf 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -142,5 +142,9 @@ public interface Attributes {
         public long asLong(int idx) {
             return Long.parseLong(asString(idx));
         }
+
+        public double asDouble(int idx) {
+            return Double.parseDouble(asString(idx));
+        }
     }
 }
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index 989d228..0c23294 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -95,13 +95,13 @@ public class AuthorizationTokenService {
                 claimToken = authorizationRequest.getAccessToken();
             }
 
-            return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(claimToken, authorization.getKeycloakSession())), authorization.getKeycloakSession());
+            return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(claimToken, authorization.getKeycloakSession())), authorizationRequest.getClaims(), authorization.getKeycloakSession());
         });
         SUPPORTED_CLAIM_TOKEN_FORMATS.put("http://openid.net/specs/openid-connect-core-1_0.html#IDToken", (authorizationRequest, authorization) -> {
             try {
                 KeycloakSession keycloakSession = authorization.getKeycloakSession();
                 IDToken idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, authorization.getRealm(), authorizationRequest.getClaimToken());
-                return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), keycloakSession);
+                return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), authorizationRequest.getClaims(), keycloakSession);
             } catch (OAuthErrorException cause) {
                 throw new RuntimeException("Failed to verify ID token", cause);
             }
@@ -129,6 +129,9 @@ public class AuthorizationTokenService {
 
         try {
             PermissionTicketToken ticket = getPermissionTicket(request);
+
+            request.setClaims(ticket.getOtherClaims());
+
             ResourceServer resourceServer = getResourceServer(ticket);
             KeycloakEvaluationContext evaluationContext = createEvaluationContext(request);
             KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity());
diff --git a/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
index dec33cd..5d67819 100644
--- a/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
+++ b/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
@@ -22,15 +22,16 @@ import org.keycloak.authorization.attribute.Attributes;
 import org.keycloak.authorization.identity.Identity;
 import org.keycloak.authorization.policy.evaluation.EvaluationContext;
 import org.keycloak.models.KeycloakSession;
-import org.keycloak.representations.AccessToken;
 
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -39,10 +40,16 @@ public class DefaultEvaluationContext implements EvaluationContext {
 
     protected final KeycloakSession keycloakSession;
     protected final Identity identity;
+    private final Map<String, Object> claims;
 
     public DefaultEvaluationContext(Identity identity, KeycloakSession keycloakSession) {
-        this.keycloakSession = keycloakSession;
+        this(identity, null, keycloakSession);
+    }
+
+    public DefaultEvaluationContext(Identity identity, Map<String, Object> claims, KeycloakSession keycloakSession) {
         this.identity = identity;
+        this.claims = claims;
+        this.keycloakSession = keycloakSession;
     }
 
     @Override
@@ -51,7 +58,7 @@ public class DefaultEvaluationContext implements EvaluationContext {
     }
 
     public Map<String, Collection<String>> getBaseAttributes() {
-        HashMap<String, Collection<String>> attributes = new HashMap<>();
+        Map<String, Collection<String>> attributes = new HashMap<>();
 
         attributes.put("kc.time.date_time", Arrays.asList(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
         attributes.put("kc.client.network.ip_address", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteAddr()));
@@ -65,6 +72,20 @@ public class DefaultEvaluationContext implements EvaluationContext {
 
         attributes.put("kc.realm.name", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
 
+        if (claims != null) {
+            for (Entry<String, Object> entry : claims.entrySet()) {
+                Object value = entry.getValue();
+
+                if (value.getClass().isArray()) {
+                    attributes.put(entry.getKey(), Arrays.asList(String[].class.cast(value)));
+                } else if (value instanceof Collection) {
+                    attributes.put(entry.getKey(), Collection.class.cast(value));
+                } else {
+                    attributes.put(entry.getKey(), Arrays.asList(String.valueOf(value)));
+                }
+            }
+        }
+
         return attributes;
     }
 
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
index 047ff5a..fd43153 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
@@ -34,7 +34,11 @@ public class KeycloakEvaluationContext extends DefaultEvaluationContext {
     private final KeycloakIdentity identity;
 
     public KeycloakEvaluationContext(KeycloakIdentity identity, KeycloakSession keycloakSession) {
-        super(identity, keycloakSession);
+        this(identity, null, keycloakSession);
+    }
+
+    public KeycloakEvaluationContext(KeycloakIdentity identity, Map<String, Object> claims, KeycloakSession keycloakSession) {
+        super(identity, claims, keycloakSession);
         this.identity = identity;
     }
 
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
index 7dd3496..272ad3a 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
@@ -16,32 +16,30 @@
  */
 package org.keycloak.authorization.protection.permission;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.core.Response;
+
 import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.common.KeycloakIdentity;
 import org.keycloak.authorization.model.Resource;
 import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.model.Scope;
-import org.keycloak.models.ClientModel;
-import org.keycloak.representations.idm.authorization.PermissionRequest;
-import org.keycloak.representations.idm.authorization.PermissionResponse;
 import org.keycloak.authorization.store.ResourceStore;
 import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeyManager;
-import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
 import org.keycloak.representations.idm.authorization.PermissionTicketToken;
-import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
-import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.representations.idm.authorization.ScopeRepresentation;
 import org.keycloak.services.ErrorResponseException;
 
-import javax.ws.rs.core.Response;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
@@ -65,9 +63,9 @@ public class AbstractPermissionService {
         return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(request))).build();
     }
 
-    private List<ResourceRepresentation> verifyRequestedResource(List<PermissionRequest> request) {
+    private List<PermissionTicketToken.ResourcePermission> verifyRequestedResource(List<PermissionRequest> request) {
         ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
-        List<ResourceRepresentation> requestedResources = new ArrayList<>();
+        List<PermissionTicketToken.ResourcePermission> requestedResources = new ArrayList<>();
 
         for (PermissionRequest permissionRequest : request) {
             String resourceSetId = permissionRequest.getResourceId();
@@ -104,19 +102,10 @@ public class AbstractPermissionService {
             }
 
             if (resources.isEmpty()) {
-                requestedResources.add(new ResourceRepresentation(null, verifyRequestedScopes(permissionRequest, null)));
-
+                requestedResources.add(new PermissionTicketToken.ResourcePermission(null, verifyRequestedScopes(permissionRequest, null)));
             } else {
                 for (Resource resource : resources) {
-                    Set<ScopeRepresentation> scopes = verifyRequestedScopes(permissionRequest, resource);
-
-                    ResourceRepresentation representation = new ResourceRepresentation(resource.getName(), scopes);
-
-                    representation.setId(resource.getId());
-                    representation.setOwnerManagedAccess(resource.isOwnerManagedAccess());
-                    representation.setOwner(new ResourceOwnerRepresentation(resource.getOwner()));
-
-                    requestedResources.add(representation);
+                    requestedResources.add(new PermissionTicketToken.ResourcePermission(resource.getId(), verifyRequestedScopes(permissionRequest, resource)));
                 }
             }
         }
@@ -124,7 +113,7 @@ public class AbstractPermissionService {
         return requestedResources;
     }
 
-    private Set<ScopeRepresentation> verifyRequestedScopes(PermissionRequest request, Resource resource) {
+    private Set<String> verifyRequestedScopes(PermissionRequest request, Resource resource) {
         Set<String> requestScopes = request.getScopes();
 
         if (requestScopes == null) {
@@ -153,24 +142,28 @@ public class AbstractPermissionService {
                 throw new ErrorResponseException("invalid_scope", "Scope [" + scopeName + "] is invalid", Response.Status.BAD_REQUEST);
             }
 
-            return ModelToRepresentation.toRepresentation(scope);
+            return scope.getName();
         }).collect(Collectors.toSet());
     }
 
     private String createPermissionTicket(List<PermissionRequest> request) {
-        List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request).stream().flatMap(resource -> {
-            List<PermissionTicketToken.ResourcePermission> perms = new ArrayList<>();
-            Set<ScopeRepresentation> scopes = resource.getScopes();
-
-            perms.add(new PermissionTicketToken.ResourcePermission(resource.getId(), scopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet())));
-
-            return perms.stream();
-        }).collect(Collectors.toList());
+        List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request);
 
         KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
         ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());
+        PermissionTicketToken token = new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken());
+
+        for (PermissionRequest permissionRequest : request) {
+            Map<String, List<String>> claims = permissionRequest.getClaims();
+
+            if (claims != null) {
+                for (Entry<String, List<String>> claim : claims.entrySet()) {
+                    token.setOtherClaims(claim.getKey(), claim.getValue());
+                }
+            }
+        }
 
-        return new JWSBuilder().kid(keys.getKid()).jsonContent(new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken()))
+        return new JWSBuilder().kid(keys.getKid()).jsonContent(token)
                 .rsa256(keys.getPrivateKey());
     }
 }
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java
new file mode 100644
index 0000000..0d36913
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaPermissionTicketPushedClaimsTest extends AbstractResourceServerTest {
+
+    @Test
+    public void testEvaluatePermissionsWithPushedClaims() throws Exception {
+        ResourceRepresentation resource = addResource("Bank Account", "withdraw");
+        JSPolicyRepresentation policy = new JSPolicyRepresentation();
+
+        policy.setName("Withdraw Limit Policy");
+
+        StringBuilder code = new StringBuilder();
+
+        code.append("var context = $evaluation.getContext();");
+        code.append("var attributes = context.getAttributes();");
+        code.append("var withdrawValue = attributes.getValue('my.bank.account.withdraw.value');");
+        code.append("if (withdrawValue && withdrawValue.asDouble(0) <= 100) {");
+        code.append("   $evaluation.grant();");
+        code.append("}");
+
+        policy.setCode(code.toString());
+
+        AuthorizationResource authorization = getClient(getRealm()).authorization();
+
+        authorization.policies().js().create(policy);
+
+        ScopePermissionRepresentation representation = new ScopePermissionRepresentation();
+
+        representation.setName("Withdraw Permission");
+        representation.addScope("withdraw");
+        representation.addPolicy(policy.getName());
+
+        authorization.permissions().scope().create(representation);
+
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest permissionRequest = new PermissionRequest(resource.getId());
+
+        permissionRequest.addScope("withdraw");
+        permissionRequest.setClaim("my.bank.account.withdraw.value", "50.5");
+
+        PermissionResponse response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+        AuthorizationRequest request = new AuthorizationRequest();
+
+        request.setTicket(response.getTicket());
+        request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+        AuthorizationResponse authorizationResponse = authzClient.authorization().authorize(request);
+
+        assertNotNull(authorizationResponse);
+        assertNotNull(authorizationResponse.getToken());
+
+        permissionRequest.setClaim("my.bank.account.withdraw.value", "100.5");
+
+        response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+        request = new AuthorizationRequest();
+
+        request.setTicket(response.getTicket());
+        request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+        try {
+            authorizationResponse = authzClient.authorization().authorize(request);
+            fail("Access should be denied");
+        } catch (Exception ignore) {
+
+        }
+    }
+}