From 5a9bfea419f37267afb656ea4bfce1ff1489384f Mon Sep 17 00:00:00 2001 From: "Federico M. Facca" Date: Wed, 16 May 2018 14:59:03 +0200 Subject: [PATCH] [KEYCLOAK-7353] Support Policy Management in Protection API See https://issues.jboss.org/browse/KEYCLOAK-7353 --- .../authorization/AbstractPolicyEnforcer.java | 58 +- .../KeycloakAdapterPolicyEnforcer.java | 47 +- .../authorization/PolicyEnforcer.java | 1 + .../representation/ServerConfiguration.java | 7 + .../client/resource/PolicyResource.java | 187 ++++++ .../client/resource/ProtectionResource.java | 4 + .../client/util/HttpMethodAuthenticator.java | 1 + .../AggregatePolicyProviderFactory.java | 2 +- .../provider/client/ClientPolicyProvider.java | 8 +- .../client/ClientPolicyProviderFactory.java | 10 +- .../provider/group/GroupPolicyProvider.java | 8 +- .../group/GroupPolicyProviderFactory.java | 25 +- .../provider/js/JSPolicyProviderFactory.java | 2 +- .../ResourcePolicyProviderFactory.java | 2 +- .../ScopePolicyProviderFactory.java | 2 +- .../permission/UMAPolicyProviderFactory.java | 324 +++++++++- .../provider/role/RolePolicyProvider.java | 8 +- .../role/RolePolicyProviderFactory.java | 6 +- .../time/TimePolicyProviderFactory.java | 2 +- .../provider/user/UserPolicyProvider.java | 8 +- .../user/UserPolicyProviderFactory.java | 7 +- .../drools/DroolsPolicyProviderFactory.java | 2 +- .../keycloak/representations/AccessToken.java | 11 - .../adapters/config/PolicyEnforcerConfig.java | 12 + .../AbstractPolicyRepresentation.java | 15 + .../authorization/AuthorizationRequest.java | 10 +- .../UmaPermissionRepresentation.java | 134 +++++ .../StoreFactoryCacheSession.java | 14 +- .../jpa/store/JPAPermissionTicketStore.java | 2 + .../jpa/store/JPAPolicyStore.java | 13 +- .../jpa/store/JPAResourceStore.java | 14 +- .../jpa/store/JPAScopeStore.java | 12 +- .../authorization/AuthorizationProvider.java | 10 + .../authorization/model/PermissionTicket.java | 1 + .../permission/ResourcePermission.java | 13 + .../evaluation/DefaultPolicyEvaluator.java | 10 + ...ionTicketAwareDecisionResultCollector.java | 63 +- .../provider/PolicyProviderFactory.java | 2 +- .../authorization/store/ResourceStore.java | 11 + .../authorization/store/ScopeStore.java | 12 + .../models/utils/ModelToRepresentation.java | 7 +- .../models/utils/RepresentationToModel.java | 4 +- .../admin/PolicyEvaluationService.java | 76 +-- .../admin/PolicyResourceService.java | 30 +- .../authorization/admin/PolicyService.java | 96 ++- .../admin/PolicyTypeService.java | 2 +- .../PolicyEvaluationResponseBuilder.java | 54 +- .../AuthorizationTokenService.java | 84 +-- .../config/UmaConfiguration.java | 12 + .../protection/ProtectionService.java | 28 +- .../permission/PermissionTicketService.java | 2 + .../policy/UserManagedPermissionService.java | 183 ++++++ .../authorization/util/Permissions.java | 19 +- .../exportimport/util/ExportUtils.java | 8 +- .../freemarker/model/AuthorizationBean.java | 67 ++- .../oidc/endpoints/TokenEndpoint.java | 2 +- .../resources/account/AccountFormService.java | 109 +++- .../TestPolicyProviderFactory.java | 2 +- .../authorization/PolicyEnforcerTest.java | 106 +++- .../authz/AbstractResourceServerTest.java | 8 +- .../UserManagedPermissionServiceTest.java | 561 ++++++++++++++++++ .../account/messages/messages_en.properties | 4 + .../theme/base/account/resource-detail.ftl | 64 ++ ...esource-server-policy-evaluate-result.html | 12 +- 64 files changed, 2245 insertions(+), 375 deletions(-) create mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/resource/PolicyResource.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/authorization/UmaPermissionRepresentation.java create mode 100644 services/src/main/java/org/keycloak/authorization/protection/policy/UserManagedPermissionService.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java 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 c54191f931..20f6c90b73 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 @@ -28,6 +28,7 @@ import org.jboss.logging.Logger; import org.keycloak.AuthorizationContext; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.OIDCHttpFacade; +import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.HttpFacade.Request; import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.ClientAuthorizationContext; @@ -165,7 +166,7 @@ public abstract class AbstractPolicyEnforcer { policyEnforcer.getPathMatcher().removeFromCache(getPath(request)); } - return hasValidClaims(actualPathConfig, httpFacade, authorization); + return hasValidClaims(actualPathConfig, permission, httpFacade, authorization); } } } else { @@ -187,35 +188,22 @@ public abstract class AbstractPolicyEnforcer { return false; } - private boolean hasValidClaims(PathConfig actualPathConfig, OIDCHttpFacade httpFacade, Authorization authorization) { - Map> claimInformationPointConfig = actualPathConfig.getClaimInformationPointConfig(); + private boolean hasValidClaims(PathConfig actualPathConfig, Permission permission, OIDCHttpFacade httpFacade, Authorization authorization) { + Map> grantedClaims = permission.getClaims(); - if (claimInformationPointConfig != null) { - Map> claims = new HashMap<>(); + if (grantedClaims != null) { + Map> claims = resolveClaims(actualPathConfig, httpFacade); - for (Entry> entry : claimInformationPointConfig.entrySet()) { - ClaimInformationPointProviderFactory factory = policyEnforcer.getClaimInformationPointProviderFactories().get(entry.getKey()); - - if (factory == null) { - throw new RuntimeException("Could not find claim information provider with name [" + entry.getKey() + "]"); - } - - claims.putAll(factory.create(entry.getValue()).resolve(httpFacade)); + if (claims.isEmpty()) { + return false; } - Map> grantedClaims = authorization.getClaims(); + for (Entry> entry : grantedClaims.entrySet()) { + List requestClaims = claims.get(entry.getKey()); - if (grantedClaims != null) { - if (claims.isEmpty()) { + if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) { return false; } - for (Entry> entry : grantedClaims.entrySet()) { - List requestClaims = claims.get(entry.getKey()); - - if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) { - return false; - } - } } } @@ -342,4 +330,28 @@ public abstract class AbstractPolicyEnforcer { private PathConfig getPathConfig(Request request) { return isDefaultAccessDeniedUri(request) ? null : policyEnforcer.getPathMatcher().matches(getPath(request)); } + + protected Map> resolveClaims(PathConfig pathConfig, OIDCHttpFacade httpFacade) { + Map> claims = getClaims(getEnforcerConfig().getClaimInformationPointConfig(), httpFacade); + + claims.putAll(getClaims(pathConfig.getClaimInformationPointConfig(), httpFacade)); + + return claims; + } + + private Map> getClaims(Map>claimInformationPointConfig, HttpFacade httpFacade) { + Map> claims = new HashMap<>(); + + if (claimInformationPointConfig != null) { + for (Entry> claimDef : claimInformationPointConfig.entrySet()) { + ClaimInformationPointProviderFactory factory = getPolicyEnforcer().getClaimInformationPointProviderFactories().get(claimDef.getKey()); + + if (factory != null) { + claims.putAll(factory.create(claimDef.getValue()).resolve(httpFacade)); + } + } + } + + return claims; + } } 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 4e87c907b1..67149bd471 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 @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import org.jboss.logging.Logger; import org.keycloak.KeycloakSecurityContext; @@ -87,19 +86,6 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { grantedPermissions.add(newPermission); } } - - Map> newClaims = newAuthorization.getClaims(); - - if (newClaims != null) { - Map> claims = authorization.getClaims(); - - if (claims == null) { - claims = new HashMap<>(); - authorization.setClaims(claims); - } - - claims.putAll(newClaims); - } } original.setAuthorization(authorization); @@ -169,11 +155,11 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { String ticket = getPermissionTicket(pathConfig, methodConfig, getAuthzClient(), httpFacade); authzRequest.setTicket(ticket); } else { - if (accessToken.getAuthorization() != null) { + if (isBearerAuthorization(httpFacade) || accessToken.getAuthorization() != null) { authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes()); } - Map> claims = getClaims(pathConfig, httpFacade); + Map> claims = resolveClaims(pathConfig, httpFacade); if (!claims.isEmpty()) { authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt"); @@ -186,7 +172,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { } LOGGER.debug("Obtaining authorization for authenticated user."); - AuthorizationResponse authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest); + AuthorizationResponse authzResponse; + + if (isBearerAuthorization(httpFacade)) { + authzRequest.setSubjectToken(accessTokenString); + authzResponse = getAuthzClient().authorization().authorize(authzRequest); + } else { + authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest); + } if (authzResponse != null) { return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment); @@ -200,7 +193,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { return null; } - private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient, HttpFacade httpFacade) { + private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient, OIDCHttpFacade httpFacade) { if (getEnforcerConfig().getUserManagedAccess() != null) { ProtectionResource protection = authzClient.protection(); PermissionResource permission = protection.permission(); @@ -209,7 +202,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { permissionRequest.setResourceId(pathConfig.getId()); permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes())); - Map> claims = getClaims(pathConfig, httpFacade); + Map> claims = resolveClaims(pathConfig, httpFacade); if (!claims.isEmpty()) { permissionRequest.setClaims(claims); @@ -221,22 +214,6 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { return null; } - private Map> getClaims(PathConfig pathConfig, HttpFacade httpFacade) { - Map> claims = new HashMap<>(); - Map> claimInformationPointConfig = pathConfig.getClaimInformationPointConfig(); - - if (claimInformationPointConfig != null) { - for (Entry> claimDef : claimInformationPointConfig.entrySet()) { - ClaimInformationPointProviderFactory factory = getPolicyEnforcer().getClaimInformationPointProviderFactories().get(claimDef.getKey()); - - if (factory != null) { - claims.putAll(factory.create(claimDef.getValue()).resolve(httpFacade)); - } - } - } - return claims; - } - private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) { List authHeaders = httpFacade.getRequest().getHeaders("Authorization"); if (authHeaders == null || authHeaders.size() == 0) { 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 d23de4f9f3..3bd4070cad 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 @@ -319,6 +319,7 @@ public class PolicyEnforcer { config.setMethods(originalConfig.getMethods()); config.setParentConfig(originalConfig); config.setEnforcementMode(originalConfig.getEnforcementMode()); + config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig()); return config; } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java index f708e52af1..eabf0859a5 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java @@ -102,6 +102,9 @@ public class ServerConfiguration { @JsonProperty("permission_endpoint") private String permissionEndpoint; + + @JsonProperty("policy_endpoint") + private String policyEndpoint; public String getIssuer() { return issuer; @@ -206,4 +209,8 @@ public class ServerConfiguration { public String getPermissionEndpoint() { return permissionEndpoint; } + + public String getPolicyEndpoint() { + return policyEndpoint; + } } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PolicyResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PolicyResource.java new file mode 100644 index 0000000000..46207eb6b5 --- /dev/null +++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PolicyResource.java @@ -0,0 +1,187 @@ +/* + * 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.authorization.client.resource; + +import java.util.List; +import java.util.concurrent.Callable; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.keycloak.authorization.client.representation.ServerConfiguration; +import org.keycloak.authorization.client.util.Http; +import org.keycloak.authorization.client.util.Throwables; +import org.keycloak.authorization.client.util.TokenCallable; +import org.keycloak.representations.idm.authorization.UmaPermissionRepresentation; +import org.keycloak.util.JsonSerialization; + +/** + * An entry point for managing user-managed permissions for a particular resource + * + * @author Pedro Igor + */ +public class PolicyResource { + + private String resourceId; + private final Http http; + private final ServerConfiguration serverConfiguration; + private final TokenCallable pat; + + public PolicyResource(String resourceId, Http http, ServerConfiguration serverConfiguration, TokenCallable pat) { + this.resourceId = resourceId; + this.http = http; + this.serverConfiguration = serverConfiguration; + this.pat = pat; + } + + /** + * Creates a new user-managed permission as represented by the given {@code permission}. + * + * @param permission the permission to create + * @return if successful, the permission created + */ + public UmaPermissionRepresentation create(final UmaPermissionRepresentation permission) { + if (permission == null) { + throw new IllegalArgumentException("Permission must not be null"); + } + + Callable callable = new Callable() { + @Override + public UmaPermissionRepresentation call() throws Exception { + return http.post(serverConfiguration.getPolicyEndpoint() + "/" + resourceId) + .authorizationBearer(pat.call()) + .json(JsonSerialization.writeValueAsBytes(permission)) + .response().json(UmaPermissionRepresentation.class).execute(); + } + }; + try { + return callable.call(); + } catch (Exception cause) { + return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error creating policy for resurce [" + resourceId + "]", cause); + } + } + + /** + * Updates an existing user-managed permission + * + * @param permission the permission to update + */ + public void update(final UmaPermissionRepresentation permission) { + if (permission == null) { + throw new IllegalArgumentException("Permission must not be null"); + } + + if (permission.getId() == null) { + throw new IllegalArgumentException("Permission id must not be null"); + } + + Callable callable = new Callable() { + @Override + public Void call() throws Exception { + http.put(serverConfiguration.getPolicyEndpoint() + "/"+ permission.getId()) + .authorizationBearer(pat.call()) + .json(JsonSerialization.writeValueAsBytes(permission)).execute(); + return null; + } + }; + try { + callable.call(); + } catch (Exception cause) { + Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error updating policy for resurce [" + resourceId + "]", cause); + } + } + + /** + * Deletes an existing user-managed permission + * + * @param id the permission id + */ + public void delete(final String id) { + Callable callable = new Callable() { + @Override + public Void call() { + http.delete(serverConfiguration.getPolicyEndpoint() + "/" + id) + .authorizationBearer(pat.call()) + .response().execute(); + return null; + } + }; + try { + callable.call(); + } catch (Exception cause) { + Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error updating policy for resurce [" + resourceId + "]", cause); + } + } + + /** + * Queries the server for permission matching the given parameters. + * + * @param id the permission id + * @param name the name of the permission + * @param scope the scope associated with the permission + * @param firstResult the position of the first resource to retrieve + * @param maxResult the maximum number of resources to retrieve + * @return the permissions matching the given parameters + */ + public List find(final String name, + final String scope, + final Integer firstResult, + final Integer maxResult) { + Callable> callable = new Callable>() { + @Override + public List call() { + return http.>get(serverConfiguration.getPolicyEndpoint()) + .authorizationBearer(pat.call()) + .param("name", name) + .param("resource", resourceId) + .param("scope", scope) + .param("first", firstResult == null ? null : firstResult.toString()) + .param("max", maxResult == null ? null : maxResult.toString()) + .response().json(new TypeReference>(){}).execute(); + } + }; + try { + return callable.call(); + } catch (Exception cause) { + return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error querying policies for resource [" + resourceId + "]", cause); + } + } + + /** + * Queries the server for a permission with the given {@code id}. + * + * @param id the permission id + * @return the permission with the given id + */ + public UmaPermissionRepresentation findById(final String id) { + if (id == null) { + throw new IllegalArgumentException("Permission id must not be null"); + } + + Callable callable = new Callable() { + @Override + public UmaPermissionRepresentation call() { + return http.get(serverConfiguration.getPolicyEndpoint() + "/" + id) + .authorizationBearer(pat.call()) + .response().json(UmaPermissionRepresentation.class).execute(); + } + }; + try { + return callable.call(); + } catch (Exception cause) { + return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error creating policy for resurce [" + resourceId + "]", cause); + } + } +} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java index 03fa9451ae..08030e1be7 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java @@ -64,6 +64,10 @@ public class ProtectionResource { return new PermissionResource(http, serverConfiguration, pat); } + public PolicyResource policy(String resourceId) { + return new PolicyResource(resourceId, http, serverConfiguration, pat); + } + /** * Introspects the given rpt using the token introspection endpoint. * diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java index 33674fbc0c..e0a8a99cef 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java @@ -83,6 +83,7 @@ public class HttpMethodAuthenticator { method.param("rpt", request.getRpt()); method.param("scope", request.getScope()); method.param("audience", request.getAudience()); + method.param("subject_token", request.getSubjectToken()); if (permissions != null) { for (ResourcePermission permission : permissions.getResources()) { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java index d6c4f074c8..476d1823ae 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/aggregated/AggregatePolicyProviderFactory.java @@ -73,7 +73,7 @@ public class AggregatePolicyProviderFactory implements PolicyProviderFactory representationFunction; + private final BiFunction representationFunction; - public ClientPolicyProvider(Function representationFunction) { + public ClientPolicyProvider(BiFunction representationFunction) { this.representationFunction = representationFunction; } @Override public void evaluate(Evaluation evaluation) { - ClientPolicyRepresentation representation = representationFunction.apply(evaluation.getPolicy()); + ClientPolicyRepresentation representation = representationFunction.apply(evaluation.getPolicy(), evaluation.getAuthorizationProvider()); AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider(); RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm(); EvaluationContext context = evaluation.getContext(); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java index c118c26cc4..c48dacdf96 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/client/ClientPolicyProviderFactory.java @@ -30,7 +30,7 @@ import org.keycloak.util.JsonSerialization; public class ClientPolicyProviderFactory implements PolicyProviderFactory { - private ClientPolicyProvider provider = new ClientPolicyProvider(policy -> toRepresentation(policy)); + private ClientPolicyProvider provider = new ClientPolicyProvider(this::toRepresentation); @Override public String getName() { @@ -48,7 +48,7 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory(Arrays.asList(getClients(policy)))); return representation; @@ -75,12 +75,12 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory config = new HashMap<>(); try { - RealmModel realm = authorizationProvider.getRealm(); + RealmModel realm = authorization.getRealm(); config.put("clients", JsonSerialization.writeValueAsString(userRep.getClients().stream().map(id -> realm.getClientById(id).getClientId()).collect(Collectors.toList()))); } catch (IOException cause) { throw new RuntimeException("Failed to export user policy [" + policy.getName() + "]", cause); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java index 5f4fcd8da9..fa76a6dc2f 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java @@ -19,7 +19,7 @@ package org.keycloak.authorization.policy.provider.group; import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath; import java.util.List; -import java.util.function.Function; +import java.util.function.BiFunction; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.attribute.Attributes; @@ -36,16 +36,16 @@ import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation; */ public class GroupPolicyProvider implements PolicyProvider { - private final Function representationFunction; + private final BiFunction representationFunction; - public GroupPolicyProvider(Function representationFunction) { + public GroupPolicyProvider(BiFunction representationFunction) { this.representationFunction = representationFunction; } @Override public void evaluate(Evaluation evaluation) { - GroupPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy()); AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider(); + GroupPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy(), authorizationProvider); RealmModel realm = authorizationProvider.getRealm(); Attributes.Entry groupsClaim = evaluation.getContext().getIdentity().getAttributes().getValue(policy.getGroupsClaim()); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java index f18e20d80f..6f45011db4 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java @@ -43,7 +43,7 @@ import org.keycloak.util.JsonSerialization; */ public class GroupPolicyProviderFactory implements PolicyProviderFactory { - private GroupPolicyProvider provider = new GroupPolicyProvider(policy -> toRepresentation(policy)); + private GroupPolicyProvider provider = new GroupPolicyProvider(this::toRepresentation); @Override public String getId() { @@ -71,7 +71,7 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory config = new HashMap<>(); - GroupPolicyRepresentation groupPolicy = toRepresentation(policy); + GroupPolicyRepresentation groupPolicy = toRepresentation(policy, authorization); Set groups = groupPolicy.getGroups(); for (GroupPolicyRepresentation.GroupDefinition definition: groups) { - GroupModel group = authorizationProvider.getRealm().getGroupById(definition.getId()); + GroupModel group = authorization.getRealm().getGroupById(definition.getId()); definition.setId(null); definition.setPath(ModelToRepresentation.buildGroupPath(group)); } try { - config.put("groupsClaim", groupPolicy.getGroupsClaim()); + String groupsClaim = groupPolicy.getGroupsClaim(); + + if (groupsClaim != null) { + config.put("groupsClaim", groupsClaim); + } + config.put("groups", JsonSerialization.writeValueAsString(groups)); } catch (IOException cause) { throw new RuntimeException("Failed to export group policy [" + policy.getName() + "]", cause); @@ -147,17 +152,15 @@ public class GroupPolicyProviderFactory implements PolicyProviderFactory groups, AuthorizationProvider authorization) { - if (groupsClaim == null) { - throw new RuntimeException("Group claims property not provided"); - } - if (groups == null || groups.isEmpty()) { throw new RuntimeException("You must provide at least one group"); } Map config = new HashMap<>(policy.getConfig()); - config.put("groupsClaim", groupsClaim); + if (groupsClaim != null) { + config.put("groupsClaim", groupsClaim); + } List topLevelGroups = authorization.getRealm().getTopLevelGroups(); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java index 1b2aa16abf..5de5db5a29 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java @@ -43,7 +43,7 @@ public class JSPolicyProviderFactory implements PolicyProviderFactoryPedro Igor */ -public class UMAPolicyProviderFactory implements PolicyProviderFactory { +public class UMAPolicyProviderFactory implements PolicyProviderFactory { private UMAPolicyProvider provider = new UMAPolicyProvider(); @@ -57,53 +75,249 @@ public class UMAPolicyProviderFactory implements PolicyProviderFactory()); + public void onCreate(Policy policy, UmaPermissionRepresentation representation, AuthorizationProvider authorization) { + policy.setOwner(representation.getOwner()); + PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); + Set roles = representation.getRoles(); + + if (roles != null) { + for (String role : roles) { + createRolePolicy(policy, policyStore, role, representation.getOwner()); + } + } + + Set groups = representation.getGroups(); + + if (groups != null) { + for (String group : groups) { + createGroupPolicy(policy, policyStore, group, representation.getOwner()); + } + } + + Set clients = representation.getClients(); + + if (clients != null) { + for (String client : clients) { + createClientPolicy(policy, policyStore, client, representation.getOwner()); + } + } + + String condition = representation.getCondition(); + + if (condition != null) { + createJSPolicy(policy, policyStore, condition, representation.getOwner()); + } } @Override - public void onUpdate(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { - verifyCircularReference(policy, new ArrayList<>()); + public void onUpdate(Policy policy, UmaPermissionRepresentation representation, AuthorizationProvider authorization) { + PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); + Set associatedPolicies = policy.getAssociatedPolicies(); + + for (Policy associatedPolicy : associatedPolicies) { + AbstractPolicyRepresentation associatedRep = ModelToRepresentation.toRepresentation(associatedPolicy, authorization, false, false); + + if ("role".equals(associatedRep.getType())) { + RolePolicyRepresentation rep = RolePolicyRepresentation.class.cast(associatedRep); + + rep.setRoles(new HashSet<>()); + + Set updatedRoles = representation.getRoles(); + + if (updatedRoles != null) { + for (String role : updatedRoles) { + rep.addRole(role); + } + } + + if (rep.getRoles().isEmpty()) { + policyStore.delete(associatedPolicy.getId()); + } else { + RepresentationToModel.toModel(rep, authorization, associatedPolicy); + } + } else if ("js".equals(associatedRep.getType())) { + JSPolicyRepresentation rep = JSPolicyRepresentation.class.cast(associatedRep); + + if (representation.getCondition() != null) { + rep.setCode(representation.getCondition()); + RepresentationToModel.toModel(rep, authorization, associatedPolicy); + } else { + policyStore.delete(associatedPolicy.getId()); + } + } else if ("group".equals(associatedRep.getType())) { + GroupPolicyRepresentation rep = GroupPolicyRepresentation.class.cast(associatedRep); + + rep.setGroups(new HashSet<>()); + + Set updatedGroups = representation.getGroups(); + + if (updatedGroups != null) { + for (String group : updatedGroups) { + rep.addGroupPath(group); + } + } + + if (rep.getGroups().isEmpty()) { + policyStore.delete(associatedPolicy.getId()); + } else { + RepresentationToModel.toModel(rep, authorization, associatedPolicy); + } + } else if ("client".equals(associatedRep.getType())) { + ClientPolicyRepresentation rep = ClientPolicyRepresentation.class.cast(associatedRep); + + rep.setClients(new HashSet<>()); + + Set updatedClients = representation.getClients(); + + if (updatedClients != null) { + for (String client : updatedClients) { + rep.addClient(client); + } + } + + if (rep.getClients().isEmpty()) { + policyStore.delete(associatedPolicy.getId()); + } else { + RepresentationToModel.toModel(rep, authorization, associatedPolicy); + } + } + } + + Set updatedRoles = representation.getRoles(); + + if (updatedRoles != null) { + boolean createPolicy = true; + + for (Policy associatedPolicy : associatedPolicies) { + if ("role".equals(associatedPolicy.getType())) { + createPolicy = false; + } + } + + if (createPolicy) { + for (String role : updatedRoles) { + createRolePolicy(policy, policyStore, role, policy.getOwner()); + } + } + } + + Set updatedGroups = representation.getGroups(); + + if (updatedGroups != null) { + boolean createPolicy = true; + + for (Policy associatedPolicy : associatedPolicies) { + if ("group".equals(associatedPolicy.getType())) { + createPolicy = false; + } + } + + if (createPolicy) { + for (String group : updatedGroups) { + createGroupPolicy(policy, policyStore, group, policy.getOwner()); + } + } + } + + Set updatedClients = representation.getClients(); + + if (updatedClients != null) { + boolean createPolicy = true; + + for (Policy associatedPolicy : associatedPolicies) { + if ("client".equals(associatedPolicy.getType())) { + createPolicy = false; + } + } + + if (createPolicy) { + for (String client : updatedClients) { + createClientPolicy(policy, policyStore, client, policy.getOwner()); + } + } + } + + String condition = representation.getCondition(); + + if (condition != null) { + boolean createPolicy = true; + + for (Policy associatedPolicy : associatedPolicies) { + if ("js".equals(associatedPolicy.getType())) { + createPolicy = false; + } + } + + if (createPolicy) { + createJSPolicy(policy, policyStore, condition, policy.getOwner()); + } + } } @Override public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) { - verifyCircularReference(policy, new ArrayList<>()); } @Override - public PolicyRepresentation toRepresentation(Policy policy) { - return new PolicyRepresentation(); + public UmaPermissionRepresentation toRepresentation(Policy policy, AuthorizationProvider authorization) { + UmaPermissionRepresentation representation = new UmaPermissionRepresentation(); + + representation.setScopes(policy.getScopes().stream().map(Scope::getName).collect(Collectors.toSet())); + representation.setOwner(policy.getOwner()); + + for (Policy associatedPolicy : policy.getAssociatedPolicies()) { + AbstractPolicyRepresentation associatedRep = ModelToRepresentation.toRepresentation(associatedPolicy, authorization, false, false); + RealmModel realm = authorization.getRealm(); + + if ("role".equals(associatedRep.getType())) { + RolePolicyRepresentation rep = RolePolicyRepresentation.class.cast(associatedRep); + + for (RoleDefinition definition : rep.getRoles()) { + RoleModel role = realm.getRoleById(definition.getId()); + + if (role.isClientRole()) { + representation.addClientRole(ClientModel.class.cast(role.getContainer()).getClientId(),role.getName()); + } else { + representation.addRole(role.getName()); + } + } + } else if ("js".equals(associatedRep.getType())) { + JSPolicyRepresentation rep = JSPolicyRepresentation.class.cast(associatedRep); + representation.setCondition(rep.getCode()); + } else if ("group".equals(associatedRep.getType())) { + GroupPolicyRepresentation rep = GroupPolicyRepresentation.class.cast(associatedRep); + + for (GroupDefinition definition : rep.getGroups()) { + representation.addGroup(ModelToRepresentation.buildGroupPath(realm.getGroupById(definition.getId()))); + } + } else if ("client".equals(associatedRep.getType())) { + ClientPolicyRepresentation rep = ClientPolicyRepresentation.class.cast(associatedRep); + + for (String client : rep.getClients()) { + representation.addClient(realm.getClientById(client).getClientId()); + } + } + } + + return representation; } @Override - public Class getRepresentationType() { - return PolicyRepresentation.class; - } - - private void verifyCircularReference(Policy policy, List ids) { - if (!policy.getType().equals("uma")) { - return; - } - - if (ids.contains(policy.getId())) { - throw new RuntimeException("Circular reference found [" + policy.getName() + "]."); - } - - ids.add(policy.getId()); - - for (Policy associated : policy.getAssociatedPolicies()) { - verifyCircularReference(associated, ids); - } + public Class getRepresentationType() { + return UmaPermissionRepresentation.class; } @Override public void onRemove(Policy policy, AuthorizationProvider authorization) { + PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); + for (Policy associatedPolicy : policy.getAssociatedPolicies()) { + policyStore.delete(associatedPolicy.getId()); + } } @Override @@ -125,4 +339,56 @@ public class UMAPolicyProviderFactory implements PolicyProviderFactory representationFunction; + private final BiFunction representationFunction; - public RolePolicyProvider(Function representationFunction) { + public RolePolicyProvider(BiFunction representationFunction) { this.representationFunction = representationFunction; } @Override public void evaluate(Evaluation evaluation) { Policy policy = evaluation.getPolicy(); - Set roleIds = representationFunction.apply(policy).getRoles(); + Set roleIds = representationFunction.apply(policy, evaluation.getAuthorizationProvider()).getRoles(); AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider(); RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm(); Identity identity = evaluation.getContext().getIdentity(); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java index bfd3e96c1a..7565e2471b 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java @@ -52,7 +52,7 @@ import java.util.Set; */ public class RolePolicyProviderFactory implements PolicyProviderFactory { - private RolePolicyProvider provider = new RolePolicyProvider(policy -> toRepresentation(policy)); + private RolePolicyProvider provider = new RolePolicyProvider(this::toRepresentation); @Override public String getName() { @@ -75,7 +75,7 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory config = new HashMap<>(); - Set roles = toRepresentation(policy).getRoles(); + Set roles = toRepresentation(policy, authorizationProvider).getRoles(); for (RolePolicyRepresentation.RoleDefinition roleDefinition : roles) { RoleModel role = authorizationProvider.getRealm().getRoleById(roleDefinition.getId()); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java index ffaf6ce707..c1e3f4116a 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java @@ -66,7 +66,7 @@ public class TimePolicyProviderFactory implements PolicyProviderFactory config = policy.getConfig(); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java index f891257079..3e1f9b7384 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProvider.java @@ -17,8 +17,10 @@ */ package org.keycloak.authorization.policy.provider.user; +import java.util.function.BiFunction; import java.util.function.Function; +import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.evaluation.Evaluation; import org.keycloak.authorization.policy.evaluation.EvaluationContext; @@ -30,16 +32,16 @@ import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; */ public class UserPolicyProvider implements PolicyProvider { - private final Function representationFunction; + private final BiFunction representationFunction; - public UserPolicyProvider(Function representationFunction) { + public UserPolicyProvider(BiFunction representationFunction) { this.representationFunction = representationFunction; } @Override public void evaluate(Evaluation evaluation) { EvaluationContext context = evaluation.getContext(); - UserPolicyRepresentation representation = representationFunction.apply(evaluation.getPolicy()); + UserPolicyRepresentation representation = representationFunction.apply(evaluation.getPolicy(), evaluation.getAuthorizationProvider()); for (String userId : representation.getUsers()) { if (context.getIdentity().getId().equals(userId)) { diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java index 9ae349d2ea..6f4c0bf74d 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/user/UserPolicyProviderFactory.java @@ -25,7 +25,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import org.keycloak.Config; @@ -52,7 +51,7 @@ import org.keycloak.util.JsonSerialization; */ public class UserPolicyProviderFactory implements PolicyProviderFactory { - private UserPolicyProvider provider = new UserPolicyProvider((Function) policy -> toRepresentation(policy)); + private UserPolicyProvider provider = new UserPolicyProvider(this::toRepresentation); @Override public String getName() { @@ -75,7 +74,7 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory config = new HashMap<>(); try { diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java index a879aad764..800259ad4d 100644 --- a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java +++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java @@ -51,7 +51,7 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory permissions; - @JsonProperty("claims") - private Map> claims; - public List getPermissions() { return permissions; } @@ -98,14 +95,6 @@ public class AccessToken extends IDToken { public void setPermissions(List permissions) { this.permissions = permissions; } - - public void setClaims(Map> claims) { - this.claims = claims; - } - - public Map> getClaims() { - return claims; - } } @JsonProperty("trusted-certs") 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 81dc5063e0..ae448d8a03 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 @@ -54,6 +54,10 @@ public class PolicyEnforcerConfig { @JsonInclude(JsonInclude.Include.NON_NULL) private UserManagedAccessConfig userManagedAccess; + @JsonProperty("claim-information-point") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Map> claimInformationPointConfig; + public List getPaths() { return this.paths; } @@ -102,6 +106,14 @@ public class PolicyEnforcerConfig { this.onDenyRedirectTo = onDenyRedirectTo; } + public Map> getClaimInformationPointConfig() { + return claimInformationPointConfig; + } + + public void setClaimInformationPointConfig(Map> config) { + this.claimInformationPointConfig = config; + } + public static class PathConfig { public static PathConfig createPathConfig(ResourceRepresentation resourceDescription) { diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java index ada763c9fc..8d7bbeb9ba 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java @@ -35,6 +35,7 @@ public class AbstractPolicyRepresentation { private Set scopes; private Logic logic = Logic.POSITIVE; private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; + private String owner; public String getId() { return this.id; @@ -135,6 +136,20 @@ public class AbstractPolicyRepresentation { this.scopes.addAll(Arrays.asList(id)); } + public void removeScope(String scope) { + if (scopes != null) { + scopes.remove(scope); + } + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + @Override public boolean equals(final Object o) { if (this == o) return true; 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 14f1f3d664..668207580f 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 @@ -39,7 +39,7 @@ public class AuthorizationRequest { private PermissionTicketToken permissions = new PermissionTicketToken(); private Metadata metadata; private String audience; - private String accessToken; + private String subjectToken; private boolean submitRequest; private Map> claims; @@ -123,12 +123,12 @@ public class AuthorizationRequest { return audience; } - public void setAccessToken(String accessToken) { - this.accessToken = accessToken; + public void setSubjectToken(String subjectToken) { + this.subjectToken = subjectToken; } - public String getAccessToken() { - return accessToken; + public String getSubjectToken() { + return subjectToken; } public Map> getClaims() { diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/UmaPermissionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/UmaPermissionRepresentation.java new file mode 100644 index 0000000000..a7bcceaf3d --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/UmaPermissionRepresentation.java @@ -0,0 +1,134 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.representations.idm.authorization; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Federico M. Facca + */ +public class UmaPermissionRepresentation extends AbstractPolicyRepresentation { + + private String id; + private String description; + private Set roles; + private Set groups; + private Set clients; + private String condition; + + @Override + public String getType() { + return "uma"; + } + + public void setId(String id){ + this.id = id; + } + + public String getId(){ + return id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public void addRole(String... role) { + if (roles == null) { + roles = new HashSet<>(); + } + + roles.addAll(Arrays.asList(role)); + } + + public void addClientRole(String clientId, String roleName) { + addRole(clientId + "/" + roleName); + } + + public void removeRole(String role) { + if (roles != null) { + roles.remove(role); + } + } + + public Set getRoles() { + return roles; + } + + public void setGroups(Set groups) { + this.groups = groups; + } + + public void addGroup(String... group) { + if (groups == null) { + groups = new HashSet<>(); + } + + groups.addAll(Arrays.asList(group)); + } + + public void removeGroup(String group) { + if (groups != null) { + groups.remove(group); + } + } + + public Set getGroups() { + return groups; + } + + public void setClients(Set clients) { + this.clients = clients; + } + + public void addClient(String... client) { + if (clients == null) { + clients = new HashSet<>(); + } + + clients.addAll(Arrays.asList(client)); + } + + public void removeClient(String client) { + if (clients != null) { + clients.remove(client); + } + } + + public Set getClients() { + return clients; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public String getCondition() { + return condition; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java index b08629abc2..06e9f25938 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java @@ -454,7 +454,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { protected class ScopeCache implements ScopeStore { @Override public Scope create(String name, ResourceServer resourceServer) { - Scope scope = getScopeStoreDelegate().create(name, resourceServer); + return create(null, name, resourceServer); + } + + @Override + public Scope create(String id, String name, ResourceServer resourceServer) { + Scope scope = getScopeStoreDelegate().create(id, name, resourceServer); registerScopeInvalidation(scope.getId(), scope.getName(), resourceServer.getId()); return scope; } @@ -538,7 +543,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { protected class ResourceCache implements ResourceStore { @Override public Resource create(String name, ResourceServer resourceServer, String owner) { - Resource resource = getResourceStoreDelegate().create(name, resourceServer, owner); + return create(null, name, resourceServer, owner); + } + + @Override + public Resource create(String id, String name, ResourceServer resourceServer, String owner) { + Resource resource = getResourceStoreDelegate().create(id, name, resourceServer, owner); Resource cached = findById(resource.getId(), resourceServer.getId()); registerResourceInvalidation(resource.getId(), resource.getName(), resource.getType(), resource.getUri(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resourceServer.getId(), resource.getOwner()); return cached; diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java index 3ab4da99a3..1e7f825544 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java @@ -206,6 +206,8 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { predicates.add(builder.isNull(root.get("requester"))); } else if (PermissionTicket.POLICY_IS_NOT_NULL.equals(name)) { predicates.add(builder.isNotNull(root.get("policy"))); + } else if (PermissionTicket.POLICY.equals(name)) { + predicates.add(root.join("policy").get("id").in(value)); } else { throw new RuntimeException("Unsupported filter [" + name + "]"); } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java index 021f451347..66a31d0a51 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java @@ -58,7 +58,12 @@ public class JPAPolicyStore implements PolicyStore { public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) { PolicyEntity entity = new PolicyEntity(); - entity.setId(KeycloakModelUtils.generateId()); + if (representation.getId() == null) { + entity.setId(KeycloakModelUtils.generateId()); + } else { + entity.setId(representation.getId()); + } + entity.setType(representation.getType()); entity.setName(representation.getName()); entity.setResourceServer(ResourceServerAdapter.toEntity(entityManager, resourceServer)); @@ -136,9 +141,9 @@ public class JPAPolicyStore implements PolicyStore { attributes.forEach((name, value) -> { if ("permission".equals(name)) { if (Boolean.valueOf(value[0])) { - predicates.add(root.get("type").in("resource", "scope")); + predicates.add(root.get("type").in("resource", "scope", "uma")); } else { - predicates.add(builder.not(root.get("type").in("resource", "scope"))); + predicates.add(builder.not(root.get("type").in("resource", "scope", "uma"))); } } else if ("id".equals(name)) { predicates.add(root.get(name).in(value)); @@ -148,6 +153,8 @@ public class JPAPolicyStore implements PolicyStore { predicates.add(builder.isNotNull(root.get("owner"))); } else if ("resource".equals(name)) { predicates.add(root.join("resources").get("id").in(value)); + } else if ("scope".equals(name)) { + predicates.add(root.join("scopes").get("id").in(value)); } else { predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%")); } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java index 7f6338d506..38ddb800ad 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java @@ -53,9 +53,19 @@ public class JPAResourceStore implements ResourceStore { @Override public Resource create(String name, ResourceServer resourceServer, String owner) { + return create(null, name, resourceServer, owner); + } + + @Override + public Resource create(String id, String name, ResourceServer resourceServer, String owner) { ResourceEntity entity = new ResourceEntity(); - entity.setId(KeycloakModelUtils.generateId()); + if (id == null) { + entity.setId(KeycloakModelUtils.generateId()); + } else { + entity.setId(id); + } + entity.setName(name); entity.setResourceServer(ResourceServerAdapter.toEntity(entityManager, resourceServer)); entity.setOwner(owner); @@ -185,6 +195,8 @@ public class JPAResourceStore implements ResourceStore { predicates.add(builder.equal(builder.lower(root.get(name)), value[0].toLowerCase())); } else if ("uri_not_null".equals(name)) { predicates.add(builder.isNotNull(root.get("uri"))); + } else if ("owner".equals(name)) { + predicates.add(root.get(name).in(value)); } else { predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%")); } diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java index befde658ac..c7debc307d 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java @@ -54,9 +54,19 @@ public class JPAScopeStore implements ScopeStore { @Override public Scope create(final String name, final ResourceServer resourceServer) { + return create(null, name, resourceServer); + } + + @Override + public Scope create(String id, final String name, final ResourceServer resourceServer) { ScopeEntity entity = new ScopeEntity(); - entity.setId(KeycloakModelUtils.generateId()); + if (id == null) { + entity.setId(KeycloakModelUtils.generateId()); + } else { + entity.setId(id); + } + entity.setName(name); entity.setResourceServer(ResourceServerAdapter.toEntity(entityManager, resourceServer)); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java index 57f4c9373c..25888b8e07 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java @@ -232,6 +232,11 @@ public final class AuthorizationProvider implements Provider { return delegate.create(name, resourceServer); } + @Override + public Scope create(String id, String name, ResourceServer resourceServer) { + return delegate.create(id, name, resourceServer); + } + @Override public void delete(String id) { Scope scope = findById(id, null); @@ -411,6 +416,11 @@ public final class AuthorizationProvider implements Provider { return delegate.create(name, resourceServer, owner); } + @Override + public Resource create(String id, String name, ResourceServer resourceServer, String owner) { + return delegate.create(id, name, resourceServer, owner); + } + @Override public void delete(String id) { Resource resource = findById(id, null); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java index 493bfc278a..c011a11bdb 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java @@ -30,6 +30,7 @@ public interface PermissionTicket { String REQUESTER = "requester"; String REQUESTER_IS_NULL = "requester_is_null"; String POLICY_IS_NOT_NULL = "policy_is_not_null"; + String POLICY = "policy"; /** * Returns the unique identifier for this instance. diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java index efd3c27ba8..3789281567 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java @@ -22,11 +22,14 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; /** @@ -42,9 +45,19 @@ public class ResourcePermission { private Map> claims; public ResourcePermission(Resource resource, List scopes, ResourceServer resourceServer) { + this(resource, scopes, resourceServer, null); + } + + public ResourcePermission(Resource resource, List scopes, ResourceServer resourceServer, Map> claims) { this.resource = resource; this.scopes = scopes; this.resourceServer = resourceServer; + if (claims != null) { + this.claims = new HashMap<>(); + for (Entry> entry : claims.entrySet()) { + this.claims.computeIfAbsent(entry.getKey(), key -> new LinkedHashSet<>()).addAll(entry.getValue()); + } + } } /** diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java index 24127d6405..dd1a81a763 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java @@ -192,6 +192,16 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator { } } + if (policyResources.isEmpty() && scopes.isEmpty()) { + String defaultResourceType = policy.getConfig().get("defaultResourceType"); + + if (defaultResourceType == null) { + return false; + } + + return defaultResourceType.equals(permission.getResource().getType()); + } + return false; } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java index d86888de79..6ab219b35f 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java @@ -16,8 +16,8 @@ */ package org.keycloak.authorization.policy.evaluation; -import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.identity.Identity; import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; @@ -57,6 +58,50 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult this.authorization = authorization; } + @Override + public void onDecision(DefaultEvaluation evaluation) { + super.onDecision(evaluation); + removePermissionsIfGranted(evaluation); + } + + /** + * Removes permissions (represented by {@code ticket}) granted by any user-managed policy so we don't create unnecessary permission tickets. + * + * @param evaluation the evaluation + */ + private void removePermissionsIfGranted(DefaultEvaluation evaluation) { + if (Effect.PERMIT.equals(evaluation.getEffect())) { + Policy policy = evaluation.getParentPolicy(); + + if ("uma".equals(policy.getType())) { + ResourcePermission grantedPermission = evaluation.getPermission(); + List permissions = ticket.getResources(); + + Iterator itPermissions = permissions.iterator(); + + while (itPermissions.hasNext()) { + PermissionTicketToken.ResourcePermission permission = itPermissions.next(); + + if (permission.getResourceId().equals(grantedPermission.getResource().getId())) { + Set scopes = permission.getScopes(); + Iterator itScopes = scopes.iterator(); + + while (itScopes.hasNext()) { + Scope scope = authorization.getStoreFactory().getScopeStore().findByName(itScopes.next(), resourceServer.getId()); + if (policy.getScopes().contains(scope)) { + itScopes.remove(); + } + } + + if (scopes.isEmpty()) { + itPermissions.remove(); + } + } + } + } + } + } + @Override public void onComplete() { super.onComplete(); @@ -64,17 +109,17 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult if (request.isSubmitRequest()) { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); - List resources = ticket.getResources(); + List permissions = ticket.getResources(); - if (resources != null) { - for (PermissionTicketToken.ResourcePermission permission : resources) { + if (permissions != null) { + for (PermissionTicketToken.ResourcePermission permission : permissions) { Resource resource = resourceStore.findById(permission.getResourceId(), resourceServer.getId()); if (resource == null) { resource = resourceStore.findByName(permission.getResourceId(), identity.getId(), resourceServer.getId()); } - if (!resource.isOwnerManagedAccess() || resource.getOwner().equals(identity.getId()) || resource.getOwner().equals(resourceServer.getId())) { + if (resource == null || !resource.isOwnerManagedAccess() || resource.getOwner().equals(identity.getId()) || resource.getOwner().equals(resourceServer.getId())) { continue; } @@ -91,9 +136,9 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult filters.put(PermissionTicket.REQUESTER, identity.getId()); filters.put(PermissionTicket.SCOPE_IS_NULL, Boolean.TRUE.toString()); - List permissions = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1); + List tickets = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1); - if (permissions.isEmpty()) { + if (tickets.isEmpty()) { authorization.getStoreFactory().getPermissionTicketStore().create(resource.getId(), null, identity.getId(), resource.getResourceServer()); } } else { @@ -112,9 +157,9 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult filters.put(PermissionTicket.REQUESTER, identity.getId()); filters.put(PermissionTicket.SCOPE, scope.getId()); - List permissions = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1); + List tickets = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1); - if (permissions.isEmpty()) { + if (tickets.isEmpty()) { authorization.getStoreFactory().getPermissionTicketStore().create(resource.getId(), scope.getId(), identity.getId(), resource.getResourceServer()); } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java index 1d353fdcd2..2d3ab76ea6 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java @@ -40,7 +40,7 @@ public interface PolicyProviderFactory e PolicyProvider create(AuthorizationProvider authorization); - R toRepresentation(Policy policy); + R toRepresentation(Policy policy, AuthorizationProvider authorization); Class getRepresentationType(); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java index fd6f85c4d1..78d55db5d2 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java @@ -40,6 +40,17 @@ public interface ResourceStore { */ Resource create(String name, ResourceServer resourceServer, String owner); + /** + *

Creates a {@link Resource} instance backed by this persistent storage implementation. + * + * @param id the id of this resource. It must be unique. + * @param name the name of this resource. It must be unique. + * @param resourceServer the resource server to where the given resource belongs to + * @param owner the owner of this resource or null if the resource server is the owner + * @return an instance backed by the underlying storage implementation + */ + Resource create(String id, String name, ResourceServer resourceServer, String owner); + /** * Removes a {@link Resource} instance, with the given {@code id} from the persistent storage. * diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java index fa9e70d9af..011b6ab2fe 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/ScopeStore.java @@ -42,6 +42,18 @@ public interface ScopeStore { */ Scope create(String name, ResourceServer resourceServer); + /** + * Creates a new {@link Scope} instance. The new instance is not necessarily persisted though, which may require + * a call to the {#save} method to actually make it persistent. + * + * @param id the id of the scope + * @param name the name of the scope + * @param resourceServer the resource server to which this scope belongs + * + * @return a new instance of {@link Scope} + */ + Scope create(String id, String name, ResourceServer resourceServer); + /** * Deletes a scope from the underlying persistence mechanism. * diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index d35eb8d9c9..9bd2f8efdf 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -17,6 +17,8 @@ package org.keycloak.models.utils; +import com.fasterxml.jackson.core.type.TypeReference; +import java.io.IOException; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.Policy; @@ -24,7 +26,6 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; -import org.keycloak.authorization.store.ResourceStore; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.Time; import org.keycloak.component.ComponentModel; @@ -37,9 +38,11 @@ import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.idm.*; import org.keycloak.representations.idm.authorization.*; import org.keycloak.storage.StorageId; +import org.keycloak.util.JsonSerialization; import java.util.*; import java.util.stream.Collectors; +import java.util.Map; /** * @author Bill Burke @@ -777,7 +780,7 @@ public class ModelToRepresentation { } } else { try { - representation = (R) providerFactory.toRepresentation(policy); + representation = (R) providerFactory.toRepresentation(policy, authorization); } catch (Exception cause) { throw new RuntimeException("Could not create policy [" + policy.getType() + "] representation", cause); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index fe449cd794..99d34054f9 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -2381,7 +2381,7 @@ public class RepresentationToModel { return existing; } - Resource model = resourceStore.create(resource.getName(), resourceServer, ownerId); + Resource model = resourceStore.create(resource.getId(), resource.getName(), resourceServer, ownerId); model.setDisplayName(resource.getDisplayName()); model.setType(resource.getType()); @@ -2426,7 +2426,7 @@ public class RepresentationToModel { return existing; } - Scope model = scopeStore.create(scope.getName(), resourceServer); + Scope model = scopeStore.create(scope.getId(), scope.getName(), resourceServer); model.setDisplayName(scope.getDisplayName()); model.setIconUri(scope.getIconUri()); diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java index 3ba29ddbd9..3e3fa2777f 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java @@ -19,11 +19,11 @@ package org.keycloak.authorization.admin; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,11 +34,9 @@ import java.util.stream.Stream; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuthErrorException; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.admin.representation.PolicyEvaluationResponseBuilder; @@ -49,7 +47,6 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.permission.ResourcePermission; -import org.keycloak.authorization.policy.evaluation.DecisionResultCollector; import org.keycloak.authorization.policy.evaluation.EvaluationContext; import org.keycloak.authorization.policy.evaluation.Result; import org.keycloak.authorization.store.ScopeStore; @@ -62,10 +59,10 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; @@ -90,36 +87,34 @@ public class PolicyEvaluationService { this.auth = auth; } - static class Decision extends DecisionResultCollector { - Throwable error; - List results; - - @Override - protected void onComplete(List results) { - this.results = results; - } - - @Override - public void onError(Throwable cause) { - this.error = cause; - - } - } - - public static List asList(T... a) { - List list = new LinkedList(); - for (T t : a) list.add(t); - return list; - } - @POST @Consumes("application/json") @Produces("application/json") - public Response evaluate(PolicyEvaluationRequest evaluationRequest) throws Throwable { + public Response evaluate(PolicyEvaluationRequest evaluationRequest) { this.auth.realm().requireViewAuthorization(); CloseableKeycloakIdentity identity = createIdentity(evaluationRequest); try { - return Response.ok(PolicyEvaluationResponseBuilder.build(evaluate(evaluationRequest, createEvaluationContext(evaluationRequest, identity)), resourceServer, authorization, identity)).build(); + AuthorizationRequest request = new AuthorizationRequest(); + Map> claims = new HashMap<>(); + Map givenAttributes = evaluationRequest.getContext().get("attributes"); + + if (givenAttributes != null) { + givenAttributes.forEach((key, entryValue) -> { + if (entryValue != null) { + List values = new ArrayList(); + + for (String value : entryValue.split(",")) { + values.add(value); + } + + claims.put(key, values); + } + }); + } + + request.setClaims(claims); + + return Response.ok(PolicyEvaluationResponseBuilder.build(evaluate(evaluationRequest, createEvaluationContext(evaluationRequest, identity), request), resourceServer, authorization, identity)).build(); } catch (Exception e) { throw new ErrorResponseException(OAuthErrorException.SERVER_ERROR, "Error while evaluating permissions.", Status.INTERNAL_SERVER_ERROR); } finally { @@ -127,8 +122,8 @@ public class PolicyEvaluationService { } } - private List evaluate(PolicyEvaluationRequest evaluationRequest, EvaluationContext evaluationContext) { - return authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization), evaluationContext).evaluate(); + private List evaluate(PolicyEvaluationRequest evaluationRequest, EvaluationContext evaluationContext, AuthorizationRequest request) { + return authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization, request), evaluationContext).evaluate(); } private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation, KeycloakIdentity identity) { @@ -157,7 +152,7 @@ public class PolicyEvaluationService { }; } - private List createPermissions(PolicyEvaluationRequest representation, EvaluationContext evaluationContext, AuthorizationProvider authorization) { + private List createPermissions(PolicyEvaluationRequest representation, EvaluationContext evaluationContext, AuthorizationProvider authorization, AuthorizationRequest request) { List resources = representation.getResources(); return resources.stream().flatMap((Function>) resource -> { StoreFactory storeFactory = authorization.getStoreFactory(); @@ -175,18 +170,18 @@ public class PolicyEvaluationService { if (resource.getId() != null) { Resource resourceModel = storeFactory.getResourceStore().findById(resource.getId(), resourceServer.getId()); - return Permissions.createResourcePermissions(resourceModel, scopeNames, authorization).stream(); + return Arrays.asList(Permissions.createResourcePermissions(resourceModel, scopeNames, authorization, request)).stream(); } else if (resource.getType() != null) { - return storeFactory.getResourceStore().findByType(resource.getType(), resourceServer.getId()).stream().flatMap(resource1 -> Permissions.createResourcePermissions(resource1, scopeNames, authorization).stream()); + return storeFactory.getResourceStore().findByType(resource.getType(), resourceServer.getId()).stream().map(resource1 -> Permissions.createResourcePermissions(resource1, scopeNames, authorization, request)); } else { ScopeStore scopeStore = storeFactory.getScopeStore(); List scopes = scopeNames.stream().map(scopeName -> scopeStore.findByName(scopeName, this.resourceServer.getId())).collect(Collectors.toList()); - List collect = new ArrayList(); + List collect = new ArrayList<>(); if (!scopes.isEmpty()) { - collect.addAll(scopes.stream().map(scope -> new ResourcePermission(null, asList(scope), resourceServer)).collect(Collectors.toList())); + collect.addAll(scopes.stream().map(scope -> new ResourcePermission(null, Arrays.asList(scope), resourceServer)).collect(Collectors.toList())); } else { - collect.addAll(Permissions.all(resourceServer, evaluationContext.getIdentity(), authorization)); + collect.addAll(Permissions.all(resourceServer, evaluationContext.getIdentity(), authorization, request)); } return collect.stream(); @@ -266,13 +261,6 @@ public class PolicyEvaluationService { } AccessToken.Access realmAccess = accessToken.getRealmAccess(); - Map claims = accessToken.getOtherClaims(); - Map givenAttributes = representation.getContext().get("attributes"); - - if (givenAttributes != null) { - givenAttributes.forEach((key, value) -> claims.put(key, asList(value))); - } - if (representation.getRoleIds() != null) { representation.getRoleIds().forEach(roleName -> realmAccess.addRole(roleName)); diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java index f4df4c44a7..c89c340de7 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java @@ -73,8 +73,10 @@ public class PolicyResourceService { @Consumes("application/json") @Produces("application/json") @NoCache - public Response update(@Context UriInfo uriInfo,String payload) { - this.auth.realm().requireManageAuthorization(); + public Response update(@Context UriInfo uriInfo, String payload) { + if (auth != null) { + this.auth.realm().requireManageAuthorization(); + } AbstractPolicyRepresentation representation = doCreateRepresentation(payload); @@ -94,7 +96,9 @@ public class PolicyResourceService { @DELETE public Response delete(@Context UriInfo uriInfo) { - this.auth.realm().requireManageAuthorization(); + if (auth != null) { + this.auth.realm().requireManageAuthorization(); + } if (policy == null) { return Response.status(Status.NOT_FOUND).build(); @@ -119,7 +123,9 @@ public class PolicyResourceService { @Produces("application/json") @NoCache public Response findById() { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } if (policy == null) { return Response.status(Status.NOT_FOUND).build(); @@ -137,7 +143,9 @@ public class PolicyResourceService { @Produces("application/json") @NoCache public Response getDependentPolicies() { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } if (policy == null) { return Response.status(Status.NOT_FOUND).build(); @@ -161,7 +169,9 @@ public class PolicyResourceService { @Produces("application/json") @NoCache public Response getScopes() { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } if (policy == null) { return Response.status(Status.NOT_FOUND).build(); @@ -182,7 +192,9 @@ public class PolicyResourceService { @Produces("application/json") @NoCache public Response getResources() { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } if (policy == null) { return Response.status(Status.NOT_FOUND).build(); @@ -203,7 +215,9 @@ public class PolicyResourceService { @Produces("application/json") @NoCache public Response getAssociatedPolicies() { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } if (policy == null) { return Response.status(Status.NOT_FOUND).build(); diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java index cb87631581..20a3e698d8 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java @@ -24,6 +24,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import javax.ws.rs.Consumes; @@ -43,10 +45,14 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.policy.provider.PolicyProviderAdminService; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.StoreFactory; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -103,7 +109,9 @@ public class PolicyService { @Produces(MediaType.APPLICATION_JSON) @NoCache public Response create(@Context UriInfo uriInfo, String payload) { - this.auth.realm().requireManageAuthorization(); + if (auth != null) { + this.auth.realm().requireManageAuthorization(); + } AbstractPolicyRepresentation representation = doCreateRepresentation(payload); Policy policy = create(representation); @@ -143,7 +151,10 @@ public class PolicyService { @Produces(MediaType.APPLICATION_JSON) @NoCache public Response findByName(@QueryParam("name") String name) { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } + StoreFactory storeFactory = authorization.getStoreFactory(); if (name == null) { @@ -168,9 +179,12 @@ public class PolicyService { @QueryParam("resource") String resource, @QueryParam("scope") String scope, @QueryParam("permission") Boolean permission, + @QueryParam("owner") String owner, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResult) { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } Map search = new HashMap<>(); @@ -186,42 +200,56 @@ public class PolicyService { search.put("type", new String[] {type}); } + if (owner != null && !"".equals(owner.trim())) { + search.put("owner", new String[] {owner}); + } + StoreFactory storeFactory = authorization.getStoreFactory(); - PolicyStore policyStore = storeFactory.getPolicyStore(); - if (resource != null || scope != null) { - List policies = new ArrayList<>(); + if (resource != null && !"".equals(resource.trim())) { + ResourceStore resourceStore = storeFactory.getResourceStore(); + Resource resourceModel = resourceStore.findById(resource, resourceServer.getId()); - if (resource != null && !"".equals(resource.trim())) { - HashMap resourceSearch = new HashMap<>(); + if (resourceModel == null) { + Map resourceFilters = new HashMap<>(); - resourceSearch.put("name", new String[]{resource}); + resourceFilters.put("name", new String[]{resource}); - storeFactory.getResourceStore().findByResourceServer(resourceSearch, resourceServer.getId(), -1, 1).forEach(resource1 -> { - policies.addAll(policyStore.findByResource(resource1.getId(), resourceServer.getId())); - if (resource1.getType() != null) { - policies.addAll(policyStore.findByResourceType(resource1.getType(), resourceServer.getId())); - } - }); + if (owner != null) { + resourceFilters.put("owner", new String[]{owner}); + } + + Set resources = resourceStore.findByResourceServer(resourceFilters, resourceServer.getId(), -1, 1).stream().map(Resource::getId).collect(Collectors.toSet()); + + if (resources.isEmpty()) { + return Response.ok().build(); + } + + search.put("resource", resources.toArray(new String[resources.size()])); + } else { + search.put("resource", new String[] {resourceModel.getId()}); } + } - if (scope != null && !"".equals(scope.trim())) { - HashMap scopeSearch = new HashMap<>(); + if (scope != null && !"".equals(scope.trim())) { + ScopeStore scopeStore = storeFactory.getScopeStore(); + Scope scopeModel = scopeStore.findById(scope, resourceServer.getId()); - scopeSearch.put("name", new String[]{scope}); + if (scopeModel == null) { + Map scopeFilters = new HashMap<>(); - storeFactory.getScopeStore().findByResourceServer(scopeSearch, resourceServer.getId(), -1, 1).forEach(scope1 -> { - policies.addAll(policyStore.findByScopeIds(Arrays.asList(scope1.getId()), resourceServer.getId())); - }); + scopeFilters.put("name", new String[]{scope}); + + Set scopes = scopeStore.findByResourceServer(scopeFilters, resourceServer.getId(), -1, 1).stream().map(Scope::getId).collect(Collectors.toSet()); + + if (scopes.isEmpty()) { + return Response.ok().build(); + } + + search.put("scope", scopes.toArray(new String[scopes.size()])); + } else { + search.put("scope", new String[] {scopeModel.getId()}); } - - if (policies.isEmpty()) { - return Response.ok(Collections.emptyList()).build(); - } - - new ArrayList<>(policies).forEach(policy -> findAssociatedPolicies(policy, policies)); - - search.put("id", policies.stream().map(Policy::getId).toArray(String[]::new)); } if (permission != null) { @@ -249,7 +277,10 @@ public class PolicyService { @Produces(MediaType.APPLICATION_JSON) @NoCache public Response findPolicyProviders() { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } + return Response.ok( authorization.getProviderFactories().stream() .filter(factory -> !factory.isInternal()) @@ -268,7 +299,10 @@ public class PolicyService { @Path("evaluate") public PolicyEvaluationService getPolicyEvaluateResource() { - this.auth.realm().requireViewAuthorization(); + if (auth != null) { + this.auth.realm().requireViewAuthorization(); + } + PolicyEvaluationService resource = new PolicyEvaluationService(this.resourceServer, this.authorization, this.auth); ResteasyProviderFactory.getInstance().injectProperties(resource); diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java index 29118773c3..6acc57236b 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java @@ -41,7 +41,7 @@ public class PolicyTypeService extends PolicyService { private final String type; - PolicyTypeService(String type, ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + public PolicyTypeService(String type, ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { super(resourceServer, authorization, auth, adminEvent); this.type = type; } diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java index b5023b56fd..3842a94132 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java @@ -19,12 +19,15 @@ package org.keycloak.authorization.admin.representation; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.Decision; import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.policy.evaluation.Result; import org.keycloak.authorization.util.Permissions; import org.keycloak.models.ClientModel; -import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.authorization.DecisionEffect; import org.keycloak.representations.idm.authorization.PolicyEvaluationResponse; @@ -169,33 +172,64 @@ public class PolicyEvaluationResponseBuilder { return response; } - private static PolicyEvaluationResponse.PolicyResultRepresentation toRepresentation(Result.PolicyResult policy, AuthorizationProvider authorization) { + private static PolicyEvaluationResponse.PolicyResultRepresentation toRepresentation(Result.PolicyResult result, AuthorizationProvider authorization) { PolicyEvaluationResponse.PolicyResultRepresentation policyResultRep = new PolicyEvaluationResponse.PolicyResultRepresentation(); PolicyRepresentation representation = new PolicyRepresentation(); + Policy policy = result.getPolicy(); - representation.setId(policy.getPolicy().getId()); - representation.setName(policy.getPolicy().getName()); - representation.setType(policy.getPolicy().getType()); - representation.setDecisionStrategy(policy.getPolicy().getDecisionStrategy()); + representation.setId(policy.getId()); + representation.setName(policy.getName()); + representation.setType(policy.getType()); + representation.setDecisionStrategy(policy.getDecisionStrategy()); + representation.setDescription(policy.getDescription()); - representation.setResources(policy.getPolicy().getResources().stream().map(resource -> resource.getName()).collect(Collectors.toSet())); + if ("uma".equals(representation.getType())) { + Map filters = new HashMap<>(); - Set scopeNames = policy.getPolicy().getScopes().stream().map(scope -> scope.getName()).collect(Collectors.toSet()); + filters.put(PermissionTicket.POLICY, policy.getId()); + + List tickets = authorization.getStoreFactory().getPermissionTicketStore().find(filters, policy.getResourceServer().getId(), -1, 1); + + if (!tickets.isEmpty()) { + KeycloakSession keycloakSession = authorization.getKeycloakSession(); + PermissionTicket ticket = tickets.get(0); + UserModel owner = keycloakSession.users().getUserById(ticket.getOwner(), authorization.getRealm()); + UserModel requester = keycloakSession.users().getUserById(ticket.getRequester(), authorization.getRealm()); + + representation.setDescription("Resource owner (" + getUserEmailOrUserName(owner) + ") grants access to " + getUserEmailOrUserName(requester)); + } else { + String description = representation.getDescription(); + + if (description != null) { + representation.setDescription(description + " (User-Managed Policy)"); + } else { + representation.setDescription("User-Managed Policy"); + } + } + } + + representation.setResources(policy.getResources().stream().map(resource -> resource.getName()).collect(Collectors.toSet())); + + Set scopeNames = policy.getScopes().stream().map(scope -> scope.getName()).collect(Collectors.toSet()); representation.setScopes(scopeNames); policyResultRep.setPolicy(representation); - if (policy.getStatus() == Decision.Effect.DENY) { + if (result.getStatus() == Decision.Effect.DENY) { policyResultRep.setStatus(DecisionEffect.DENY); policyResultRep.setScopes(representation.getScopes()); } else { policyResultRep.setStatus(DecisionEffect.PERMIT); } - policyResultRep.setAssociatedPolicies(policy.getAssociatedPolicies().stream().map(result -> toRepresentation(result, authorization)).collect(Collectors.toList())); + policyResultRep.setAssociatedPolicies(result.getAssociatedPolicies().stream().map(policy1 -> toRepresentation(policy1, authorization)).collect(Collectors.toList())); return policyResultRep; } + + private static String getUserEmailOrUserName(UserModel user) { + return (user.getEmail() != null ? user.getEmail() : user.getUsername()); + } } 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 b791f7fd21..a0351e1e8a 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -100,7 +100,7 @@ public class AuthorizationTokenService { try { Map claims = JsonSerialization.readValue(Base64Url.decode(authorizationRequest.getClaimToken()), Map.class); authorizationRequest.setClaims(claims); - return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(authorizationRequest.getAccessToken(), authorization.getKeycloakSession())), claims, authorization.getKeycloakSession()); + return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(authorizationRequest.getSubjectToken(), authorization.getKeycloakSession())), claims, authorization.getKeycloakSession()); } catch (IOException cause) { throw new RuntimeException("Failed to map claims from claim token [" + claimToken + "]", cause); } @@ -112,7 +112,7 @@ public class AuthorizationTokenService { try { KeycloakSession keycloakSession = authorization.getKeycloakSession(); RealmModel realm = authorization.getRealm(); - String accessToken = authorizationRequest.getAccessToken(); + String accessToken = authorizationRequest.getSubjectToken(); if (accessToken == null) { throw new RuntimeException("Claim token can not be null and must be a valid IDToken"); @@ -161,7 +161,7 @@ public class AuthorizationTokenService { List evaluation; if (ticket.getResources().isEmpty() && request.getRpt() == null) { - evaluation = evaluateAllPermissions(resourceServer, evaluationContext, identity); + evaluation = evaluateAllPermissions(request, resourceServer, evaluationContext, identity); } else if(!request.getPermissions().getResources().isEmpty()) { evaluation = evaluatePermissions(request, ticket, resourceServer, evaluationContext, identity); } else { @@ -212,9 +212,9 @@ public class AuthorizationTokenService { .evaluate(new PermissionTicketAwareDecisionResultCollector(request, ticket, identity, resourceServer, authorization)).results(); } - private List evaluateAllPermissions(ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) { + private List evaluateAllPermissions(AuthorizationRequest request, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) { return authorization.evaluators() - .from(Permissions.all(resourceServer, identity, authorization), evaluationContext) + .from(Permissions.all(resourceServer, identity, authorization, request), evaluationContext) .evaluate(); } @@ -235,7 +235,6 @@ public class AuthorizationTokenService { Authorization authorization = new Authorization(); authorization.setPermissions(entitlements); - authorization.setClaims(request.getClaims()); rpt.setAuthorization(authorization); @@ -309,8 +308,9 @@ public class AuthorizationTokenService { private List createPermissions(PermissionTicketToken ticket, AuthorizationRequest request, ResourceServer resourceServer, KeycloakIdentity identity, AuthorizationProvider authorization) { StoreFactory storeFactory = authorization.getStoreFactory(); - Map> permissionsToEvaluate = new LinkedHashMap<>(); + Map permissionsToEvaluate = new LinkedHashMap<>(); ResourceStore resourceStore = storeFactory.getResourceStore(); + ScopeStore scopeStore = storeFactory.getScopeStore(); Metadata metadata = request.getMetadata(); Integer limit = metadata != null ? metadata.getLimit() : null; @@ -359,31 +359,37 @@ public class AuthorizationTokenService { requestedScopes.addAll(Arrays.asList(clientAdditionalScopes.split(" "))); } + List requestedScopesModel = requestedScopes.stream().map(s -> scopeStore.findByName(s, resourceServer.getId())).collect(Collectors.toList()); + if (!existingResources.isEmpty()) { for (Resource resource : existingResources) { - Set scopes = permissionsToEvaluate.get(resource.getId()); + ResourcePermission permission = permissionsToEvaluate.get(resource.getId()); - if (scopes == null) { - scopes = new HashSet<>(); - permissionsToEvaluate.put(resource.getId(), scopes); + if (permission == null) { + permission = Permissions.createResourcePermissions(resource, requestedScopes, authorization, request); + permissionsToEvaluate.put(resource.getId(), permission); if (limit != null) { limit--; } + } else { + for (Scope scope : requestedScopesModel) { + if (!permission.getScopes().contains(scope)) { + permission.getScopes().add(scope); + } + } } - - scopes.addAll(requestedScopes); } } else { - List resources = resourceStore.findByScope(new ArrayList<>(requestedScopes), ticket.getAudience()[0]); + List resources = resourceStore.findByScope(new ArrayList<>(requestedScopes), resourceServer.getId()); for (Resource resource : resources) { - permissionsToEvaluate.put(resource.getId(), requestedScopes); + permissionsToEvaluate.put(resource.getId(), Permissions.createResourcePermissions(resource, requestedScopes, authorization, request)); if (limit != null) { limit--; } } - permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", requestedScopes); + permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", new ResourcePermission(null, requestedScopesModel, resourceServer, request.getClaims())); } } @@ -409,28 +415,42 @@ public class AuthorizationTokenService { List permissions = authorizationData.getPermissions(); if (permissions != null) { - for (Permission permission : permissions) { + for (Permission grantedPermission : permissions) { if (limit != null && limit <= 0) { break; } - Resource resourcePermission = resourceStore.findById(permission.getResourceId(), ticket.getAudience()[0]); + Resource resourcePermission = resourceStore.findById(grantedPermission.getResourceId(), ticket.getAudience()[0]); if (resourcePermission != null) { - Set scopes = permissionsToEvaluate.get(resourcePermission.getId()); + ResourcePermission permission = permissionsToEvaluate.get(resourcePermission.getId()); - if (scopes == null) { - scopes = new HashSet<>(); - permissionsToEvaluate.put(resourcePermission.getId(), scopes); + if (permission == null) { + permission = new ResourcePermission(resourcePermission, new ArrayList<>(), resourceServer, grantedPermission.getClaims()); + permissionsToEvaluate.put(resourcePermission.getId(), permission); if (limit != null) { limit--; } + } else { + if (grantedPermission.getClaims() != null) { + for (Entry> entry : grantedPermission.getClaims().entrySet()) { + Set claims = permission.getClaims().get(entry.getKey()); + + if (claims != null) { + claims.addAll(entry.getValue()); + } + } + } } - Set scopePermission = permission.getScopes(); + for (String scopeName : grantedPermission.getScopes()) { + Scope scope = scopeStore.findByName(scopeName, resourceServer.getId()); - if (scopePermission != null) { - scopes.addAll(scopePermission); + if (scope != null) { + if (!permission.getScopes().contains(scope)) { + permission.getScopes().add(scope); + } + } } } } @@ -439,19 +459,7 @@ public class AuthorizationTokenService { } } - ScopeStore scopeStore = storeFactory.getScopeStore(); - - return permissionsToEvaluate.entrySet().stream() - .flatMap((Function>, Stream>) entry -> { - String key = entry.getKey(); - if ("$KC_SCOPE_PERMISSION".equals(key)) { - List scopes = entry.getValue().stream().map(scopeName -> scopeStore.findByName(scopeName, resourceServer.getId())).filter(scope -> Objects.nonNull(scope)).collect(Collectors.toList()); - return Arrays.asList(new ResourcePermission(null, scopes, resourceServer)).stream(); - } else { - Resource entryResource = resourceStore.findById(key, resourceServer.getId()); - return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream(); - } - }).collect(Collectors.toList()); + return new ArrayList<>(permissionsToEvaluate.values()); } private PermissionTicketToken verifyPermissionTicket(AuthorizationRequest request) { diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java b/services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java index 67fb296e28..f622dc4ad8 100644 --- a/services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java +++ b/services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java @@ -61,6 +61,7 @@ public class UmaConfiguration extends OIDCConfigurationRepresentation { configuration.setPermissionEndpoint(uriBuilder.clone().path(RealmsResource.class).path(RealmsResource.class, "getAuthorizationService").path(AuthorizationService.class, "getProtectionService").path(ProtectionService.class, "permission").build(realm.getName()).toString()); configuration.setResourceRegistrationEndpoint(uriBuilder.clone().path(RealmsResource.class).path(RealmsResource.class, "getAuthorizationService").path(AuthorizationService.class, "getProtectionService").path(ProtectionService.class, "resource").build(realm.getName()).toString()); + configuration.setPolicyEndpoint(uriBuilder.clone().path(RealmsResource.class).path(RealmsResource.class, "getAuthorizationService").path(AuthorizationService.class, "getProtectionService").path(ProtectionService.class, "policy").build(realm.getName()).toString()); return configuration; } @@ -70,6 +71,9 @@ public class UmaConfiguration extends OIDCConfigurationRepresentation { @JsonProperty("permission_endpoint") private String permissionEndpoint; + + @JsonProperty("policy_endpoint") + private String policyEndpoint; public String getResourceRegistrationEndpoint() { return this.resourceRegistrationEndpoint; @@ -86,4 +90,12 @@ public class UmaConfiguration extends OIDCConfigurationRepresentation { void setPermissionEndpoint(String permissionEndpoint) { this.permissionEndpoint = permissionEndpoint; } + + public String getPolicyEndpoint() { + return this.policyEndpoint; + } + + void setPolicyEndpoint(String policyEndpoint) { + this.policyEndpoint = policyEndpoint; + } } diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java index ce4dff6c5e..f4bbba6225 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java @@ -38,6 +38,7 @@ import javax.ws.rs.Path; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response.Status; import org.keycloak.authorization.protection.permission.PermissionTicketService; +import org.keycloak.authorization.protection.policy.UserManagedPermissionService; /** * @author Pedro Igor @@ -57,12 +58,7 @@ public class ProtectionService { public Object resource() { KeycloakIdentity identity = createIdentity(true); ResourceServer resourceServer = getResourceServer(identity); - RealmModel realm = authorization.getRealm(); - ClientModel client = realm.getClientById(resourceServer.getId()); - KeycloakSession keycloakSession = authorization.getKeycloakSession(); - UserModel serviceAccount = keycloakSession.users().getServiceAccount(client); - AdminEventBuilder adminEvent = new AdminEventBuilder(realm, new AdminAuth(realm, identity.getAccessToken(), serviceAccount, client), keycloakSession, clientConnection); - ResourceSetService resourceManager = new ResourceSetService(resourceServer, this.authorization, null, adminEvent.realm(realm).authClient(client).authUser(serviceAccount)); + ResourceSetService resourceManager = new ResourceSetService(resourceServer, this.authorization, null, createAdminEventBuilder(identity, resourceServer)); ResteasyProviderFactory.getInstance().injectProperties(resourceManager); @@ -73,6 +69,15 @@ public class ProtectionService { return resource; } + private AdminEventBuilder createAdminEventBuilder(KeycloakIdentity identity, ResourceServer resourceServer) { + RealmModel realm = authorization.getRealm(); + ClientModel client = realm.getClientById(resourceServer.getId()); + KeycloakSession keycloakSession = authorization.getKeycloakSession(); + UserModel serviceAccount = keycloakSession.users().getServiceAccount(client); + AdminEventBuilder adminEvent = new AdminEventBuilder(realm, new AdminAuth(realm, identity.getAccessToken(), serviceAccount, client), keycloakSession, clientConnection); + return adminEvent.realm(realm).authClient(client).authUser(serviceAccount); + } + @Path("/permission") public Object permission() { KeycloakIdentity identity = createIdentity(false); @@ -94,6 +99,17 @@ public class ProtectionService { return resource; } + + @Path("/uma-policy") + public Object policy() { + KeycloakIdentity identity = createIdentity(false); + + UserManagedPermissionService resource = new UserManagedPermissionService(identity, getResourceServer(identity), this.authorization, createAdminEventBuilder(identity, getResourceServer(identity))); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } private KeycloakIdentity createIdentity(boolean checkProtectionScope) { KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java index 59ff1fddb8..fafbd7142a 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java @@ -125,6 +125,8 @@ public class PermissionTicketService { throw new ErrorResponseException("invalid_permission", "Permission already exists", Response.Status.BAD_REQUEST); PermissionTicket ticket = ticketStore.create(resource.getId(), scope.getId(), user.getId(), resourceServer); + if(representation.isGranted()) + ticket.setGrantedTimestamp(java.lang.System.currentTimeMillis()); representation = ModelToRepresentation.toRepresentation(ticket, authorization); return Response.ok(representation).build(); } diff --git a/services/src/main/java/org/keycloak/authorization/protection/policy/UserManagedPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/policy/UserManagedPermissionService.java new file mode 100644 index 0000000000..d9663a487c --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/policy/UserManagedPermissionService.java @@ -0,0 +1,183 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.policy; + +import java.io.IOException; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +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.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.OAuthErrorException; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.PermissionService; +import org.keycloak.authorization.admin.PolicyTypeResourceService; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.representations.idm.authorization.UmaPermissionRepresentation; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.util.JsonSerialization; + +/** + * @author Federico M. Facca + */ +public class UserManagedPermissionService { + + private final ResourceServer resourceServer; + private final Identity identity; + private final AuthorizationProvider authorization; + private final PermissionService delegate; + + public UserManagedPermissionService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization, AdminEventBuilder eventBuilder) { + this.identity = identity; + this.resourceServer = resourceServer; + this.authorization = authorization; + delegate = new PermissionService(resourceServer, authorization, null, eventBuilder); + ResteasyProviderFactory.getInstance().injectProperties(delegate); + } + + @POST + @Path("{resourceId}") + @Consumes("application/json") + @Produces("application/json") + public Response create(@Context UriInfo uriInfo, @PathParam("resourceId") String resourceId, UmaPermissionRepresentation representation) { + if (representation.getId() != null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Newly created uma policies should not have an id", Response.Status.BAD_REQUEST); + } + + checkRequest(resourceId, representation); + + representation.addResource(resourceId); + representation.setOwner(identity.getId()); + + return findById(delegate.create(representation).getId()); + } + + @Path("{policyId}") + @PUT + @Consumes("application/json") + @Produces("application/json") + public Response update(@Context UriInfo uriInfo, @PathParam("policyId") String policyId, String payload) { + UmaPermissionRepresentation representation; + + try { + representation = JsonSerialization.readValue(payload, UmaPermissionRepresentation.class); + } catch (IOException e) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Failed to parse representation", Status.BAD_REQUEST); + } + + checkRequest(getAssociatedResourceId(policyId), representation); + + return PolicyTypeResourceService.class.cast(delegate.getResource(policyId)).update(uriInfo, payload); + } + + @Path("{policyId}") + @DELETE + public Response delete(@Context UriInfo uriInfo, @PathParam("policyId") String policyId) { + checkRequest(getAssociatedResourceId(policyId), null); + PolicyTypeResourceService.class.cast(delegate.getResource(policyId)).delete(uriInfo); + return Response.noContent().build(); + } + + @Path("{policyId}") + @GET + @Produces("application/json") + public Response findById(@PathParam("policyId") String policyId) { + checkRequest(getAssociatedResourceId(policyId), null); + return PolicyTypeResourceService.class.cast(delegate.getResource(policyId)).findById(); + } + + @GET + @NoCache + @Produces("application/json") + public Response find(@QueryParam("name") String name, + @QueryParam("resource") String resource, + @QueryParam("scope") String scope, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResult) { + return delegate.findAll(null, name, "uma", resource, scope, true, identity.getId(), firstResult, maxResult); + } + + private Policy getPolicy(@PathParam("policyId") String policyId) { + Policy existing = authorization.getStoreFactory().getPolicyStore().findById(policyId, resourceServer.getId()); + + if (existing == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Policy with [" + policyId + "] does not exist", Status.NOT_FOUND); + } + + return existing; + } + + private void checkRequest(String resourceId, UmaPermissionRepresentation representation) { + ResourceStore resourceStore = this.authorization.getStoreFactory().getResourceStore(); + Resource resource = resourceStore.findById(resourceId, resourceServer.getId()); + + if (resource == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Resource [" + resourceId + "] cannot be found", Response.Status.BAD_REQUEST); + } + + if (!resource.getOwner().equals(identity.getId())) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Only resource onwer can access policies for resource [" + resourceId + "]", Status.BAD_REQUEST); + } + + if (!resource.isOwnerManagedAccess()) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Only resources with owner managed accessed can have policies", Status.BAD_REQUEST); + } + + if (!resourceServer.isAllowRemoteResourceManagement()) { + throw new ErrorResponseException(OAuthErrorException.REQUEST_NOT_SUPPORTED, "Remote Resource Management not enabled on resource server [" + resourceServer.getId() + "]", Status.FORBIDDEN); + } + + if (representation != null) { + Set resourceScopes = resource.getScopes().stream().map(scope -> scope.getName()).collect(Collectors.toSet()); + Set scopes = representation.getScopes(); + + if (scopes == null || scopes.isEmpty()) { + scopes = resourceScopes; + representation.setScopes(scopes); + } + + if (!resourceScopes.containsAll(scopes)) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Some of the scopes [" + scopes + "] are not valid for resource [" + resourceId + "]", Response.Status.BAD_REQUEST); + } + } + } + + private String getAssociatedResourceId(String policyId) { + return getPolicy(policyId).getResources().iterator().next().getId(); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java index 180ec33e04..32219dd7c1 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -44,6 +44,7 @@ import org.keycloak.authorization.policy.evaluation.Result; import org.keycloak.authorization.store.ResourceStore; import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata; import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.services.ErrorResponseException; @@ -68,23 +69,23 @@ public final class Permissions { * @param authorization * @return */ - public static List all(ResourceServer resourceServer, Identity identity, AuthorizationProvider authorization) { + public static List all(ResourceServer resourceServer, Identity identity, AuthorizationProvider authorization, AuthorizationRequest request) { List permissions = new ArrayList<>(); StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); // obtain all resources where owner is the resource server - resourceStore.findByOwner(resourceServer.getId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization))); + resourceStore.findByOwner(resourceServer.getId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization, request))); // obtain all resources where owner is the current user - resourceStore.findByOwner(identity.getId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization))); + resourceStore.findByOwner(identity.getId(), resourceServer.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissionsWithScopes(resource, new LinkedList(resource.getScopes()), authorization, request))); // obtain all resources granted to the user via permission tickets (uma) List tickets = storeFactory.getPermissionTicketStore().findGranted(identity.getId(), resourceServer.getId()); Map userManagedPermissions = new HashMap<>(); for (PermissionTicket ticket : tickets) { - userManagedPermissions.computeIfAbsent(ticket.getResource().getId(), id -> new ResourcePermission(ticket.getResource(), new ArrayList<>(), resourceServer)); + userManagedPermissions.computeIfAbsent(ticket.getResource().getId(), id -> new ResourcePermission(ticket.getResource(), new ArrayList<>(), resourceServer, request.getClaims())); } permissions.addAll(userManagedPermissions.values()); @@ -92,8 +93,7 @@ public final class Permissions { return permissions; } - public static List createResourcePermissions(Resource resource, Set requestedScopes, AuthorizationProvider authorization) { - List permissions = new ArrayList<>(); + public static ResourcePermission createResourcePermissions(Resource resource, Set requestedScopes, AuthorizationProvider authorization, AuthorizationRequest request) { String type = resource.getType(); ResourceServer resourceServer = resource.getResourceServer(); List scopes; @@ -127,12 +127,11 @@ public final class Permissions { return byName; }).collect(Collectors.toList()); } - permissions.add(new ResourcePermission(resource, scopes, resource.getResourceServer())); - return permissions; + return new ResourcePermission(resource, scopes, resource.getResourceServer(), request.getClaims()); } - public static List createResourcePermissionsWithScopes(Resource resource, List scopes, AuthorizationProvider authorization) { + public static List createResourcePermissionsWithScopes(Resource resource, List scopes, AuthorizationProvider authorization, AuthorizationRequest request) { List permissions = new ArrayList<>(); String type = resource.getType(); ResourceServer resourceServer = resource.getResourceServer(); @@ -153,7 +152,7 @@ public final class Permissions { }); } - permissions.add(new ResourcePermission(resource, scopes, resource.getResourceServer())); + permissions.add(new ResourcePermission(resource, scopes, resource.getResourceServer(), request.getClaims())); return permissions; } diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index a84765b61e..169b5bf2b2 100755 --- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -320,7 +320,6 @@ public class ExportUtils { } else { rep.getOwner().setId(null); } - rep.setId(null); rep.getScopes().forEach(scopeRepresentation -> { scopeRepresentation.setId(null); scopeRepresentation.setIconUri(null); @@ -335,10 +334,10 @@ public class ExportUtils { PolicyStore policyStore = storeFactory.getPolicyStore(); policies.addAll(policyStore.findByResourceServer(settingsModel.getId()) - .stream().filter(policy -> !policy.getType().equals("resource") && !policy.getType().equals("scope")) + .stream().filter(policy -> !policy.getType().equals("resource") && !policy.getType().equals("scope") && policy.getOwner() == null) .map(policy -> createPolicyRepresentation(authorization, policy)).collect(Collectors.toList())); policies.addAll(policyStore.findByResourceServer(settingsModel.getId()) - .stream().filter(policy -> policy.getType().equals("resource") || policy.getType().equals("scope")) + .stream().filter(policy -> (policy.getType().equals("resource") || policy.getType().equals("scope") && policy.getOwner() == null)) .map(policy -> createPolicyRepresentation(authorization, policy)).collect(Collectors.toList())); representation.setPolicies(policies); @@ -346,7 +345,6 @@ public class ExportUtils { List scopes = storeFactory.getScopeStore().findByResourceServer(settingsModel.getId()).stream().map(scope -> { ScopeRepresentation rep = toRepresentation(scope); - rep.setId(null); rep.setPolicies(null); rep.setResources(null); @@ -362,8 +360,6 @@ public class ExportUtils { try { PolicyRepresentation rep = toRepresentation(policy, authorizationProvider, true, true); - rep.setId(null); - Map config = new HashMap<>(rep.getConfig()); rep.setConfig(config); diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java index 837a843f85..4ffe503547 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java @@ -19,6 +19,7 @@ package org.keycloak.forms.account.freemarker.model; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -30,6 +31,7 @@ import javax.ws.rs.core.UriInfo; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.store.PermissionTicketStore; @@ -260,7 +262,7 @@ public class AuthorizationBean { if (shares == null) { Map filters = new HashMap<>(); - filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.RESOURCE, this.resource.getId()); filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); shares = toPermissionRepresentation(findPermissions(filters)); @@ -269,6 +271,31 @@ public class AuthorizationBean { return shares; } + public Collection getPolicies() { + Map filters = new HashMap<>(); + + filters.put("type", new String[] {"uma"}); + filters.put("resource", new String[] {this.resource.getId()}); + filters.put("owner", new String[] {getOwner().getId()}); + + List policies = authorization.getStoreFactory().getPolicyStore().findByResourceServer(filters, getResourceServer().getId(), -1, -1); + + if (policies.isEmpty()) { + return Collections.emptyList(); + } + + return policies.stream() + .filter(policy -> { + Map filters1 = new HashMap<>(); + + filters1.put(PermissionTicket.POLICY, policy.getId()); + + return authorization.getStoreFactory().getPermissionTicketStore().find(filters1, resourceServer.getId(), -1, 1) + .isEmpty(); + }) + .map(ManagedPermissionBean::new).collect(Collectors.toList()); + } + public ResourceServerBean getResourceServer() { return resourceServer; } @@ -326,6 +353,10 @@ public class AuthorizationBean { this.clientModel = clientModel; } + public String getId() { + return clientModel.getId(); + } + public String getName() { String name = clientModel.getName(); @@ -336,6 +367,10 @@ public class AuthorizationBean { return clientModel.getClientId(); } + public String getClientId() { + return clientModel.getClientId(); + } + public String getRedirectUri() { Set redirectUris = clientModel.getRedirectUris(); @@ -346,4 +381,34 @@ public class AuthorizationBean { return redirectUris.iterator().next(); } } + + public class ManagedPermissionBean { + + private final Policy policy; + private List policies; + + public ManagedPermissionBean(Policy policy) { + this.policy = policy; + } + + public String getId() { + return policy.getId(); + } + + public Collection getScopes() { + return policy.getScopes().stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList()); + } + + public String getDescription() { + return this.policy.getDescription(); + } + + public Collection getPolicies() { + if (this.policies == null) { + this.policies = policy.getAssociatedPolicies().stream().map(ManagedPermissionBean::new).collect(Collectors.toList()); + } + + return this.policies; + } + } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index a7ba496d61..19fb4a2743 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -1048,7 +1048,7 @@ public class TokenEndpoint { authorizationRequest.setRpt(formParams.getFirst("rpt")); authorizationRequest.setScope(formParams.getFirst("scope")); authorizationRequest.setAudience(formParams.getFirst("audience")); - authorizationRequest.setAccessToken(accessTokenString); + authorizationRequest.setSubjectToken(formParams.getFirst("subject_token") != null ? formParams.getFirst("subject_token") : accessTokenString); String submitRequest = formParams.getFirst("submit_request"); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index d316e34ae0..1355614d6d 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -20,9 +20,11 @@ import org.jboss.logging.Logger; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.authorization.store.PolicyStore; import org.keycloak.common.Profile; import org.keycloak.common.Profile.Feature; import org.keycloak.common.util.Base64Url; @@ -726,45 +728,90 @@ public class AccountFormService extends AbstractSecuredLocalService { boolean isGrant = "grant".equals(action); boolean isDeny = "deny".equals(action); boolean isRevoke = "revoke".equals(action); + boolean isRevokePolicy = "revokePolicy".equals(action); + boolean isRevokePolicyAll = "revokePolicyAll".equals(action); - Map filters = new HashMap<>(); + if (isRevokePolicy || isRevokePolicyAll) { + List ids = new ArrayList(Arrays.asList(permissionId)); + Iterator iterator = ids.iterator(); + PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); + Policy policy = null; - filters.put(PermissionTicket.RESOURCE, resource.getId()); - filters.put(PermissionTicket.REQUESTER, session.users().getUserByUsername(requester, realm).getId()); + while (iterator.hasNext()) { + String id = iterator.next(); - if (isRevoke) { - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); - } else { - filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); - } - - List tickets = ticketStore.find(filters, resource.getResourceServer().getId(), -1, -1); - Iterator iterator = tickets.iterator(); - - while (iterator.hasNext()) { - PermissionTicket ticket = iterator.next(); - - if (isGrant) { - if (permissionId != null && permissionId.length > 0 && !Arrays.asList(permissionId).contains(ticket.getId())) { - continue; - } - } - - if (isGrant && !ticket.isGranted()) { - ticket.setGrantedTimestamp(System.currentTimeMillis()); - iterator.remove(); - } else if (isDeny || isRevoke) { - if (permissionId != null && permissionId.length > 0 && Arrays.asList(permissionId).contains(ticket.getId())) { + if (!id.contains(":")) { + policy = policyStore.findById(id, client.getId()); iterator.remove(); + break; } } + + Set scopesToKeep = new HashSet<>(); + + if (isRevokePolicyAll) { + for (Scope scope : policy.getScopes()) { + policy.removeScope(scope); + } + } else { + for (String id : ids) { + scopesToKeep.add(authorization.getStoreFactory().getScopeStore().findById(id.split(":")[1], client.getId())); + } + + for (Scope scope : policy.getScopes()) { + if (!scopesToKeep.contains(scope)) { + policy.removeScope(scope); + } + } + } + + if (policy.getScopes().isEmpty()) { + for (Policy associated : policy.getAssociatedPolicies()) { + policyStore.delete(associated.getId()); + } + + policyStore.delete(policy.getId()); + } + } else { + Map filters = new HashMap<>(); + + filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.REQUESTER, session.users().getUserByUsername(requester, realm).getId()); + + if (isRevoke) { + filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + } else { + filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + } + + List tickets = ticketStore.find(filters, resource.getResourceServer().getId(), -1, -1); + Iterator iterator = tickets.iterator(); + + while (iterator.hasNext()) { + PermissionTicket ticket = iterator.next(); + + if (isGrant) { + if (permissionId != null && permissionId.length > 0 && !Arrays.asList(permissionId).contains(ticket.getId())) { + continue; + } + } + + if (isGrant && !ticket.isGranted()) { + ticket.setGrantedTimestamp(System.currentTimeMillis()); + iterator.remove(); + } else if (isDeny || isRevoke) { + if (permissionId != null && permissionId.length > 0 && Arrays.asList(permissionId).contains(ticket.getId())) { + iterator.remove(); + } + } + } + + for (PermissionTicket ticket : tickets) { + ticketStore.delete(ticket.getId()); + } } - for (PermissionTicket ticket : tickets) { - ticketStore.delete(ticket.getId()); - } - - if (isRevoke) { + if (isRevoke || isRevokePolicy || isRevokePolicyAll) { return forwardToPage("resource-detail", AccountPages.RESOURCE_DETAIL); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java index 4da1695571..b450b7609d 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java @@ -50,7 +50,7 @@ public class TestPolicyProviderFactory implements PolicyProviderFactory { } @Override - public AbstractPolicyRepresentation toRepresentation(Policy policy) { + public AbstractPolicyRepresentation toRepresentation(Policy policy, AuthorizationProvider authorization) { return new PolicyRepresentation(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java index 81f2ffde8d..4713239fcb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.admin.client.authorization; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -38,6 +39,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.AuthorizationContext; import org.keycloak.KeycloakSecurityContext; +import org.keycloak.OAuth2Constants; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.OIDCHttpFacade; @@ -59,12 +61,14 @@ import org.keycloak.representations.idm.RealmRepresentation; 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.Permission; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RoleBuilder; import org.keycloak.testsuite.util.RolesBuilder; @@ -106,6 +110,10 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { .redirectUris("http://localhost/resource-server-test") .defaultRoles("uma_protection") .directAccessGrants()) + .client(ClientBuilder.create().clientId("public-client-test") + .publicClient() + .redirectUris("http://localhost:8180/auth/realms/master/app/auth/*") + .directAccessGrants()) .build()); } @@ -125,7 +133,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { headers.put("Authorization", Arrays.asList("Bearer " + token)); - AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); assertFalse(context.isGranted()); AuthorizationRequest request = new AuthorizationRequest(); @@ -137,22 +145,22 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { assertNotNull(token); - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); assertTrue(context.isGranted()); parameters.put("withdrawal.amount", Arrays.asList("200")); - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); assertFalse(context.isGranted()); parameters.put("withdrawal.amount", Arrays.asList("50")); - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); assertTrue(context.isGranted()); parameters.put("withdrawal.amount", Arrays.asList("10")); - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); request = new AuthorizationRequest(); @@ -161,8 +169,23 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { response = authzClient.authorization("marta", "password").authorize(request); token = response.getToken(); - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); assertTrue(context.isGranted()); + + request = new AuthorizationRequest(); + + request.setTicket(extractTicket(headers)); + + response = authzClient.authorization("marta", "password").authorize(request); + token = response.getToken(); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "GET", token, headers, parameters)); + assertTrue(context.isGranted()); + + assertEquals(1, context.getPermissions().size()); + Permission permission = context.getPermissions().get(0); + + assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); } @Test @@ -182,6 +205,52 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { parameters.put("withdrawal.amount", Arrays.asList("50")); + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + assertEquals(1, context.getPermissions().size()); + Permission permission = context.getPermissions().get(0); + assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); + + parameters.put("withdrawal.amount", Arrays.asList("200")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("10")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + + assertTrue(context.isGranted()); + + assertEquals(1, context.getPermissions().size()); + permission = context.getPermissions().get(0); + assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); + } + + @Test + public void testEnforceEntitlementAccessWithClaimsWithBearerToken() { + initAuthorizationSettings(getClientResource("resource-server-test")); + + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + HashMap> headers = new HashMap<>(); + HashMap> parameters = new HashMap<>(); + + AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json"); + String token = authzClient.obtainAccessToken("marta", "password").getToken(); + + headers.put("Authorization", Arrays.asList("Bearer " + token)); + + AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); assertTrue(context.isGranted()); @@ -203,7 +272,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { } @Test - public void testEnforceEntitlementAccessWithClaimsWithBearerToken() { + public void testEnforceEntitlementAccessWithClaimsWithBearerTokenFromPublicClient() { initAuthorizationSettings(getClientResource("resource-server-test")); KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); @@ -211,8 +280,13 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { HashMap> headers = new HashMap<>(); HashMap> parameters = new HashMap<>(); - AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json"); - String token = authzClient.obtainAccessToken("marta", "password").getToken(); + oauth.realm(REALM_NAME); + oauth.clientId("public-client-test"); + oauth.doLogin("marta", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); + String token = response.getAccessToken(); headers.put("Authorization", Arrays.asList("Bearer " + token)); @@ -306,7 +380,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { return clients.get(representation.getId()); } - private OIDCHttpFacade createHttpFacade(String path, String token, Map> headers, Map> parameters, InputStream requestBody) { + private OIDCHttpFacade createHttpFacade(String path, String method, String token, Map> headers, Map> parameters, InputStream requestBody) { return new OIDCHttpFacade() { Request request; Response response; @@ -325,7 +399,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { @Override public Request getRequest() { if (request == null) { - request = createHttpRequest(path, headers, parameters, requestBody); + request = createHttpRequest(path, method, headers, parameters, requestBody); } return request; } @@ -346,7 +420,11 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { } private OIDCHttpFacade createHttpFacade(String path, String token, Map> headers, Map> parameters) { - return createHttpFacade(path, token, headers, parameters, null); + return createHttpFacade(path, null, token, headers, parameters, null); + } + + private OIDCHttpFacade createHttpFacade(String path, String method, String token, Map> headers, Map> parameters) { + return createHttpFacade(path, method, token, headers, parameters, null); } private Response createHttpResponse(Map> headers) { @@ -401,14 +479,14 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { }; } - private Request createHttpRequest(String path, Map> headers, Map> parameters, InputStream requestBody) { + private Request createHttpRequest(String path, String method, Map> headers, Map> parameters, InputStream requestBody) { return new Request() { private InputStream inputStream; @Override public String getMethod() { - return "GET"; + return method == null ? "GET" : method; } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java index 237d5e3955..c5edbccb46 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java @@ -67,13 +67,17 @@ public abstract class AbstractResourceServerTest extends AbstractKeycloakTest { .user(UserBuilder.create().username("marta").password("password") .addRoles("uma_authorization", "uma_protection") .role("resource-server-test", "uma_protection")) + .user(UserBuilder.create().username("alice").password("password") + .addRoles("uma_authorization", "uma_protection") + .role("resource-server-test", "uma_protection")) .user(UserBuilder.create().username("kolo").password("password")) .client(ClientBuilder.create().clientId("resource-server-test") .secret("secret") .authorizationServicesEnabled(true) .redirectUris("http://localhost/resource-server-test") .defaultRoles("uma_protection") - .directAccessGrants()) + .directAccessGrants() + .serviceAccountsEnabled(true)) .client(ClientBuilder.create().clientId("test-app") .redirectUris("http://localhost:8180/auth/realms/master/app/auth") .publicClient()) @@ -155,7 +159,7 @@ public abstract class AbstractResourceServerTest extends AbstractKeycloakTest { return authorization.authorize(authorizationRequest); } - protected RealmResource getRealm() throws Exception { + protected RealmResource getRealm() { return adminClient.realm("authz-test"); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java new file mode 100644 index 0000000000..4abe7597c8 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java @@ -0,0 +1,561 @@ +/* + * 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.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.NotFoundException; + +import org.junit.Test; +import org.keycloak.authorization.client.AuthorizationDeniedException; +import org.keycloak.authorization.client.resource.AuthorizationResource; +import org.keycloak.authorization.client.resource.PolicyResource; +import org.keycloak.authorization.client.resource.ProtectionResource; +import org.keycloak.authorization.client.util.HttpResponseException; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.AuthorizationRequest; +import org.keycloak.representations.idm.authorization.AuthorizationResponse; +import org.keycloak.representations.idm.authorization.PermissionRequest; +import org.keycloak.representations.idm.authorization.PermissionResponse; +import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.UmaPermissionRepresentation; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.GroupBuilder; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.RolesBuilder; +import org.keycloak.testsuite.util.UserBuilder; + +/** + * @author Pedro Igor + */ +public class UserManagedPermissionServiceTest extends AbstractResourceServerTest { + + @Override + public void addTestRealms(List testRealms) { + testRealms.add(RealmBuilder.create().name(REALM_NAME) + .roles(RolesBuilder.create() + .realmRole(RoleBuilder.create().name("uma_authorization").build()) + .realmRole(RoleBuilder.create().name("uma_protection").build()) + .realmRole(RoleBuilder.create().name("role_a").build()) + .realmRole(RoleBuilder.create().name("role_b").build()) + .realmRole(RoleBuilder.create().name("role_c").build()) + .realmRole(RoleBuilder.create().name("role_d").build()) + ) + .group(GroupBuilder.create().name("group_a") + .subGroups(Arrays.asList(GroupBuilder.create().name("group_b").build())) + .build()) + .group(GroupBuilder.create().name("group_c").build()) + .user(UserBuilder.create().username("marta").password("password") + .addRoles("uma_authorization", "uma_protection") + .role("resource-server-test", "uma_protection")) + .user(UserBuilder.create().username("alice").password("password") + .addRoles("uma_authorization", "uma_protection") + .role("resource-server-test", "uma_protection")) + .user(UserBuilder.create().username("kolo").password("password") + .addRoles("role_a") + .addGroups("group_a")) + .client(ClientBuilder.create().clientId("resource-server-test") + .secret("secret") + .authorizationServicesEnabled(true) + .redirectUris("http://localhost/resource-server-test") + .defaultRoles("uma_protection") + .directAccessGrants() + .serviceAccountsEnabled(true)) + .client(ClientBuilder.create().clientId("client-a") + .redirectUris("http://localhost/resource-server-test") + .publicClient()) + .build()); + } + + @Test + public void testCreate() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName("Resource A"); + resource.setOwnerManagedAccess(true); + resource.setOwner("marta"); + resource.addScope("Scope A", "Scope B", "Scope C"); + + resource = getAuthzClient().protection().resource().create(resource); + + UmaPermissionRepresentation newPermission = new UmaPermissionRepresentation(); + + newPermission.setName("Custom User-Managed Permission"); + newPermission.setDescription("Users from specific roles are allowed to access"); + newPermission.addScope("Scope A", "Scope B", "Scope C"); + newPermission.addRole("role_a", "role_b", "role_c", "role_d"); + newPermission.addGroup("/group_a", "/group_a/group_b", "/group_c"); + newPermission.addClient("client-a", "resource-server-test"); + newPermission.setCondition("$evaluation.grant()"); + + ProtectionResource protection = getAuthzClient().protection("marta", "password"); + + UmaPermissionRepresentation permission = protection.policy(resource.getId()).create(newPermission); + + assertEquals(newPermission.getName(), permission.getName()); + assertEquals(newPermission.getDescription(), permission.getDescription()); + assertTrue(permission.getScopes().containsAll(newPermission.getScopes())); + assertTrue(permission.getRoles().containsAll(newPermission.getRoles())); + assertTrue(permission.getGroups().containsAll(newPermission.getGroups())); + assertTrue(permission.getClients().containsAll(newPermission.getClients())); + assertEquals(newPermission.getCondition(), permission.getCondition()); + } + + @Test + public void testUpdate() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName("Resource A"); + resource.setOwnerManagedAccess(true); + resource.setOwner("marta"); + resource.addScope("Scope A", "Scope B", "Scope C"); + + resource = getAuthzClient().protection().resource().create(resource); + + UmaPermissionRepresentation permission = new UmaPermissionRepresentation(); + + permission.setName("Custom User-Managed Permission"); + permission.setDescription("Users from specific roles are allowed to access"); + permission.addScope("Scope A"); + permission.addRole("role_a"); + + ProtectionResource protection = getAuthzClient().protection("marta", "password"); + + permission = protection.policy(resource.getId()).create(permission); + + assertEquals(1, getAssociatedPolicies(permission).size()); + + permission.setName("Changed"); + permission.setDescription("Changed"); + + protection.policy(resource.getId()).update(permission); + + UmaPermissionRepresentation updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertEquals(permission.getName(), updated.getName()); + assertEquals(permission.getDescription(), updated.getDescription()); + + permission.removeRole("role_a"); + permission.addRole("role_b", "role_c"); + + protection.policy(resource.getId()).update(permission); + assertEquals(1, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getRoles().containsAll(updated.getRoles())); + + permission.addRole("role_d"); + + protection.policy(resource.getId()).update(permission); + assertEquals(1, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getRoles().containsAll(updated.getRoles())); + + permission.addGroup("/group_a/group_b"); + + protection.policy(resource.getId()).update(permission); + assertEquals(2, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getGroups().containsAll(updated.getGroups())); + + permission.addGroup("/group_a"); + + protection.policy(resource.getId()).update(permission); + assertEquals(2, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getGroups().containsAll(updated.getGroups())); + + permission.removeGroup("/group_a/group_b"); + permission.addGroup("/group_c"); + + protection.policy(resource.getId()).update(permission); + assertEquals(2, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getGroups().containsAll(updated.getGroups())); + + permission.addClient("client-a"); + + protection.policy(resource.getId()).update(permission); + assertEquals(3, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getClients().containsAll(updated.getClients())); + + permission.addClient("resource-server-test"); + + protection.policy(resource.getId()).update(permission); + assertEquals(3, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getClients().containsAll(updated.getClients())); + + permission.removeClient("client-a"); + + protection.policy(resource.getId()).update(permission); + assertEquals(3, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertTrue(permission.getClients().containsAll(updated.getClients())); + + permission.setCondition("$evaluation.grant()"); + + protection.policy(resource.getId()).update(permission); + assertEquals(4, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertEquals(permission.getCondition(), updated.getCondition()); + + permission.setCondition(null); + + protection.policy(resource.getId()).update(permission); + assertEquals(3, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertEquals(permission.getCondition(), updated.getCondition()); + + permission.setRoles(null); + + protection.policy(resource.getId()).update(permission); + assertEquals(2, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertEquals(permission.getRoles(), updated.getRoles()); + + permission.setClients(null); + + protection.policy(resource.getId()).update(permission); + assertEquals(1, getAssociatedPolicies(permission).size()); + updated = protection.policy(resource.getId()).findById(permission.getId()); + + assertEquals(permission.getClients(), updated.getClients()); + + permission.setGroups(null); + + try { + protection.policy(resource.getId()).update(permission); + assertEquals(1, getAssociatedPolicies(permission).size()); + fail("Permission must be removed because the last associated policy was removed"); + } catch (NotFoundException ignore) { + + } catch (Exception e) { + fail("Expected not found"); + } + } + + @Test + public void testUserManagedPermission() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName("Resource A"); + resource.setOwnerManagedAccess(true); + resource.setOwner("marta"); + resource.addScope("Scope A", "Scope B", "Scope C"); + + resource = getAuthzClient().protection().resource().create(resource); + + UmaPermissionRepresentation permission = new UmaPermissionRepresentation(); + + permission.setName("Custom User-Managed Permission"); + permission.setDescription("Users from specific roles are allowed to access"); + permission.addScope("Scope A"); + permission.addRole("role_a"); + + ProtectionResource protection = getAuthzClient().protection("marta", "password"); + + permission = protection.policy(resource.getId()).create(permission); + + AuthorizationResource authorization = getAuthzClient().authorization("kolo", "password"); + + AuthorizationRequest request = new AuthorizationRequest(); + + request.addPermission(resource.getId(), "Scope A"); + + AuthorizationResponse authzResponse = authorization.authorize(request); + + assertNotNull(authzResponse); + + permission.removeRole("role_a"); + permission.addRole("role_b"); + + protection.policy(resource.getId()).update(permission); + + try { + authzResponse = authorization.authorize(request); + fail("User should not have permission"); + } catch (Exception e) { + assertTrue(AuthorizationDeniedException.class.isInstance(e)); + } + + try { + authzResponse = getAuthzClient().authorization("alice", "password").authorize(request); + fail("User should not have permission"); + } catch (Exception e) { + assertTrue(AuthorizationDeniedException.class.isInstance(e)); + } + + permission.addRole("role_a"); + + protection.policy(resource.getId()).update(permission); + + authzResponse = authorization.authorize(request); + + assertNotNull(authzResponse); + + protection.policy(resource.getId()).delete(permission.getId()); + + try { + authzResponse = authorization.authorize(request); + fail("User should not have permission"); + } catch (Exception e) { + assertTrue(AuthorizationDeniedException.class.isInstance(e)); + } + + try { + getAuthzClient().protection("marta", "password").policy(resource.getId()).findById(permission.getId()); + fail("Permission must not exist"); + } catch (Exception e) { + assertEquals(404, HttpResponseException.class.cast(e.getCause()).getStatusCode()); + } + } + + @Test + public void testPermissionInAdditionToUserGrantedPermission() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName("Resource A"); + resource.setOwnerManagedAccess(true); + resource.setOwner("marta"); + resource.addScope("Scope A", "Scope B", "Scope C"); + + resource = getAuthzClient().protection().resource().create(resource); + + PermissionResponse ticketResponse = getAuthzClient().protection().permission().create(new PermissionRequest(resource.getId(), "Scope A")); + + AuthorizationRequest request = new AuthorizationRequest(); + + request.setTicket(ticketResponse.getTicket()); + + try { + getAuthzClient().authorization("kolo", "password").authorize(request); + fail("User should not have permission"); + } catch (Exception e) { + assertTrue(AuthorizationDeniedException.class.isInstance(e)); + assertTrue(e.getMessage().contains("request_submitted")); + } + + List tickets = getAuthzClient().protection().permission().findByResource(resource.getId()); + + assertEquals(1, tickets.size()); + + PermissionTicketRepresentation ticket = tickets.get(0); + + ticket.setGranted(true); + + getAuthzClient().protection().permission().update(ticket); + + AuthorizationResponse authzResponse = getAuthzClient().authorization("kolo", "password").authorize(request); + + assertNotNull(authzResponse); + + UmaPermissionRepresentation permission = new UmaPermissionRepresentation(); + + permission.setName("Custom User-Managed Permission"); + permission.addScope("Scope A"); + permission.addRole("role_a"); + + ProtectionResource protection = getAuthzClient().protection("marta", "password"); + + permission = protection.policy(resource.getId()).create(permission); + + authzResponse = getAuthzClient().authorization("kolo", "password").authorize(request); + + ticket.setGranted(false); + + getAuthzClient().protection().permission().update(ticket); + + authzResponse = getAuthzClient().authorization("kolo", "password").authorize(request); + + permission = getAuthzClient().protection("marta", "password").policy(resource.getId()).findById(permission.getId()); + + assertNotNull(permission); + + permission.removeRole("role_a"); + permission.addRole("role_b"); + + getAuthzClient().protection("marta", "password").policy(resource.getId()).update(permission); + + try { + getAuthzClient().authorization("kolo", "password").authorize(request); + fail("User should not have permission"); + } catch (Exception e) { + assertTrue(AuthorizationDeniedException.class.isInstance(e)); + } + + request = new AuthorizationRequest(); + + request.addPermission(resource.getId()); + + try { + getAuthzClient().authorization("kolo", "password").authorize(request); + fail("User should not have permission"); + } catch (Exception e) { + assertTrue(AuthorizationDeniedException.class.isInstance(e)); + } + + getAuthzClient().protection("marta", "password").policy(resource.getId()).delete(permission.getId()); + + try { + getAuthzClient().authorization("kolo", "password").authorize(request); + fail("User should not have permission"); + } catch (Exception e) { + assertTrue(AuthorizationDeniedException.class.isInstance(e)); + } + } + + @Test + public void testPermissionWithoutScopes() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName(UUID.randomUUID().toString()); + resource.setOwner("marta"); + resource.setOwnerManagedAccess(true); + resource.addScope("Scope A", "Scope B", "Scope C"); + + ProtectionResource protection = getAuthzClient().protection(); + + resource = protection.resource().create(resource); + + UmaPermissionRepresentation permission = new UmaPermissionRepresentation(); + + permission.setName("Custom User-Managed Policy"); + permission.addRole("role_a"); + + PolicyResource policy = getAuthzClient().protection("marta", "password").policy(resource.getId()); + + permission = policy.create(permission); + + assertEquals(3, permission.getScopes().size()); + assertTrue(Arrays.asList("Scope A", "Scope B", "Scope C").containsAll(permission.getScopes())); + + permission = policy.findById(permission.getId()); + + assertTrue(Arrays.asList("Scope A", "Scope B", "Scope C").containsAll(permission.getScopes())); + assertEquals(3, permission.getScopes().size()); + + permission.removeScope("Scope B"); + + policy.update(permission); + permission = policy.findById(permission.getId()); + + assertEquals(2, permission.getScopes().size()); + assertTrue(Arrays.asList("Scope A", "Scope C").containsAll(permission.getScopes())); + } + + @Test + public void testOnlyResourceOwnerCanManagePolicies() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName(UUID.randomUUID().toString()); + resource.setOwner("marta"); + resource.addScope("Scope A", "Scope B", "Scope C"); + + ProtectionResource protection = getAuthzClient().protection(); + + resource = protection.resource().create(resource); + + try { + getAuthzClient().protection("alice", "password").policy(resource.getId()).create(new UmaPermissionRepresentation()); + fail("Error expected"); + } catch (Exception e) { + assertTrue(HttpResponseException.class.cast(e.getCause()).toString().contains("Only resource onwer can access policies for resource")); + } + } + + @Test + public void testOnlyResourcesWithOwnerManagedAccess() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName(UUID.randomUUID().toString()); + resource.setOwner("marta"); + resource.addScope("Scope A", "Scope B", "Scope C"); + + ProtectionResource protection = getAuthzClient().protection(); + + resource = protection.resource().create(resource); + + try { + getAuthzClient().protection("marta", "password").policy(resource.getId()).create(new UmaPermissionRepresentation()); + fail("Error expected"); + } catch (Exception e) { + assertTrue(HttpResponseException.class.cast(e.getCause()).toString().contains("Only resources with owner managed accessed can have policies")); + } + } + + @Test + public void testFindPermission() { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName(UUID.randomUUID().toString()); + resource.setOwner("marta"); + resource.setOwnerManagedAccess(true); + resource.addScope("Scope A", "Scope B", "Scope C"); + + ProtectionResource protection = getAuthzClient().protection(); + + resource = protection.resource().create(resource); + + PolicyResource policy = getAuthzClient().protection("marta", "password").policy(resource.getId()); + + for (int i = 0; i < 10; i++) { + UmaPermissionRepresentation permission = new UmaPermissionRepresentation(); + + permission.setName("Custom User-Managed Policy " + i); + permission.addRole("role_a"); + + policy.create(permission); + } + + assertEquals(10, policy.find(null, null, null, null).size()); + + List byId = policy.find("Custom User-Managed Policy 8", null, null, null); + + assertEquals(1, byId.size()); + assertEquals(byId.get(0).getId(), policy.findById(byId.get(0).getId()).getId()); + assertEquals(10, policy.find(null, "Scope A", null, null).size()); + assertEquals(5, policy.find(null, null, -1, 5).size()); + assertEquals(2, policy.find(null, null, -1, 2).size()); + } + + private List getAssociatedPolicies(UmaPermissionRepresentation permission) { + return getClient(getRealm()).authorization().policies().policy(permission.getId()).associatedPolicies(); + } + +} diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 1398730ec6..06f609456f 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -200,6 +200,10 @@ doApprove=Approve doRemoveSharing=Remove Sharing doRemoveRequest=Remove Request peopleAccessResource=People with access to this resource +resourceManagedPolicies=Permissions granting access to this resource +resourceNoPermissionsGrantingAccess=No permissions granting access to this resource +anyAction=Any action +description=Description name=Name scopes=Scopes resource=Resource diff --git a/themes/src/main/resources/theme/base/account/resource-detail.ftl b/themes/src/main/resources/theme/base/account/resource-detail.ftl index fd4e5ecb7c..e30d94781d 100755 --- a/themes/src/main/resources/theme/base/account/resource-detail.ftl +++ b/themes/src/main/resources/theme/base/account/resource-detail.ftl @@ -180,6 +180,70 @@ +

+
+

+ ${msg("resourceManagedPolicies")} +

+
+
+
+
+ + + + + + + + + + <#if authorization.resource.policies?size != 0> + <#list authorization.resource.policies as permission> + + + + + + + + + + + <#else> + + + + + +
${msg("description")}${msg("permission")}${msg("action")}
+ <#if permission.description??> + ${permission.description} + + + <#if permission.scopes?size != 0> + <#list permission.scopes as scope> + + + <#else> + ${msg("anyAction")} + + + ${msg("doRevoke")} +
+ ${msg("resourceNoPermissionsGrantingAccess")} +
+ +
+

diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html index cd8007f3a3..19ff72020b 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html @@ -41,12 +41,18 @@
  • - {{policyResult.policy.name}} + + {{policyResult.policy.name}} + + {{policyResult.policy.description}} + + decision was {{policyResult.status}} {{policyResult.status}} by {{policyResult.policy.decisionStrategy}} decision. {{policyResult.policy.scopes.length > 0 ? (policyResult.status == 'DENY' ? 'Denied Scopes:' : 'Granted Scopes:') : ''}} {{scope}}{{$last ? '' : ', '}}{{policyResult.policy.scopes.length > 0 ? '.' : ''}} -