diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 6de35b8b67..2e585c3278 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -101,7 +101,6 @@ public interface OAuth2Constants { String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token"; String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt"; String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token"; - String TOKEN_EXCHANGER ="token-exchanger"; } diff --git a/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java b/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java new file mode 100644 index 0000000000..d2c6b67672 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.authorization.common; + +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; + +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientModelIdentity implements Identity { + protected RealmModel realm; + protected ClientModel client; + protected UserModel serviceAccount; + + public ClientModelIdentity(KeycloakSession session, ClientModel client) { + this.realm = client.getRealm(); + this.client = client; + this.serviceAccount = session.users().getServiceAccount(client); + } + + @Override + public String getId() { + return client.getId(); + } + + @Override + public Attributes getAttributes() { + MultivaluedHashMap map = new MultivaluedHashMap(); + if (serviceAccount != null) map.addAll(serviceAccount.getAttributes()); + return Attributes.from(map); + } + + @Override + public boolean hasRealmRole(String roleName) { + if (serviceAccount == null) return false; + RoleModel role = realm.getRole(roleName); + if (role == null) return false; + return serviceAccount.hasRole(role); + } + + @Override + public boolean hasClientRole(String clientId, String roleName) { + if (serviceAccount == null) return false; + ClientModel client = realm.getClientByClientId(clientId); + RoleModel role = client.getRole(roleName); + if (role == null) return false; + return serviceAccount.hasRole(role); + } + + @Override + public boolean hasRole(String roleName) { + throw new RuntimeException("Should not execute"); + } + + @Override + public boolean hasClientRole(String roleName) { + throw new RuntimeException("Should not execute"); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 0581aecef2..6a26c692d5 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -683,6 +683,18 @@ public class TokenManager { this.clientSession = clientSession; } + public AccessToken getAccessToken() { + return accessToken; + } + + public RefreshToken getRefreshToken() { + return refreshToken; + } + + public IDToken getIdToken() { + return idToken; + } + public AccessTokenResponseBuilder accessToken(AccessToken accessToken) { this.accessToken = accessToken; return this; 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 28923dfd03..fef17c6ca0 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 @@ -593,45 +593,28 @@ public class TokenEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST); } - boolean allowed = false; - UserModel serviceAccount = session.users().getServiceAccount(client); - if (serviceAccount != null) { - if (authResult.getToken().getAudience() == null) { - logger.debug("Client doesn't have service account"); + boolean exchangeFromAllowed = false; + for (String aud : authResult.getToken().getAudience()) { + ClientModel audClient = realm.getClientByClientId(aud); + if (audClient == null) continue; + if (audClient.equals(client)) { + exchangeFromAllowed = true; + break; } - boolean tokenAllowed = false; - for (String aud : authResult.getToken().getAudience()) { - ClientModel audClient = realm.getClientByClientId(aud); - if (audClient == null) continue; - if (audClient.equals(client)) { - tokenAllowed = true; - break; - } - RoleModel audExchanger = audClient.getRole(OAuth2Constants.TOKEN_EXCHANGER); - if (audExchanger != null && serviceAccount.hasRole(audExchanger)) { - tokenAllowed = true; - break; - } + if (AdminPermissions.management(session, realm).clients().canExchangeFrom(client, audClient)) { + exchangeFromAllowed = true; + break; } - if (!tokenAllowed) { - logger.debug("Client does not have exchange rights for audience of token"); - } else { - RoleModel targetExchangable = targetClient.getRole(OAuth2Constants.TOKEN_EXCHANGER); - RoleModel realmExchangeable = AdminPermissions.management(session, realm).getRealmManagementClient().getRole(OAuth2Constants.TOKEN_EXCHANGER); - allowed = (targetExchangable != null && serviceAccount.hasRole(targetExchangable)) || (realmExchangeable != null && serviceAccount.hasRole(realmExchangeable)); - if (!allowed) { - logger.debug("Client does not have exchange rights for target audience"); - } - } - - } else { - logger.debug("Client doesn't have service account"); } - - if (!allowed) { + if (!exchangeFromAllowed) { + logger.debug("Client does not have exchange rights for audience of provided token"); + event.error(Errors.NOT_ALLOWED); + throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN); + } + if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) { + logger.debug("Client does not have exchange rights for target audience"); event.error(Errors.NOT_ALLOWED); throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN); - } AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false); @@ -656,6 +639,8 @@ public class TokenEndpoint { TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, session, userSession, clientSession) .generateAccessToken() .generateRefreshToken(); + responseBuilder.getAccessToken().issuedFor(client.getClientId()); + responseBuilder.getRefreshToken().issuedFor(client.getClientId()); String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE); if (TokenUtil.isOIDCRequest(scopeParam)) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java index 7df5b5e073..d8eb94a4b4 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java @@ -27,6 +27,8 @@ import org.keycloak.models.ClientModel; public interface AdminPermissionManagement { public static final String MANAGE_SCOPE = "manage"; public static final String VIEW_SCOPE = "view"; + public static final String EXCHANGE_FROM_SCOPE="exchange-from"; + public static final String EXCHANGE_TO_SCOPE="exchange-to"; ClientModel getRealmManagementClient(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java index 8a6b76dd8e..ccf9679609 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java @@ -41,6 +41,14 @@ public interface ClientPermissionManagement { Map getPermissions(ClientModel client); + boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from); + + boolean canExchangeTo(ClientModel authorizedClient, ClientModel to); + + Policy exchangeFromPermission(ClientModel client); + + Policy exchangeToPermission(ClientModel client); + Policy mapRolesPermission(ClientModel client); Policy mapRolesClientScopePermission(ClientModel client); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java index 2b1e2340c4..bbb7bf4d29 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java @@ -18,23 +18,35 @@ package org.keycloak.services.resources.admin.permissions; import org.jboss.logging.Logger; import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.common.ClientModelIdentity; +import org.keycloak.authorization.common.DefaultEvaluationContext; +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.model.Scope; +import org.keycloak.authorization.policy.evaluation.EvaluationContext; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.representations.AccessToken; import org.keycloak.services.ForbiddenException; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_FROM_SCOPE; +import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE; + /** * Manages default policies for all users. * @@ -79,6 +91,14 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa return MAP_ROLES_COMPOSITE_SCOPE + ".permission.client." + client.getId(); } + private String getExchangeToPermissionName(ClientModel client) { + return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId(); + } + + private String getExchangeFromPermissionName(ClientModel client) { + return EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId(); + } + private void initialize(ClientModel client) { ResourceServer server = root.findOrCreateResourceServer(client); Scope manageScope = manageScope(server); @@ -93,18 +113,11 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa if (mapRoleScope == null) { mapRoleScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_SCOPE, server); } - Scope mapRoleClientScope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_CLIENT_SCOPE, server.getId()); - if (mapRoleClientScope == null) { - mapRoleClientScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_CLIENT_SCOPE, server); - } - Scope mapRoleCompositeScope = authz.getStoreFactory().getScopeStore().findByName(MAP_ROLES_COMPOSITE_SCOPE, server.getId()); - if (mapRoleCompositeScope == null) { - mapRoleCompositeScope = authz.getStoreFactory().getScopeStore().create(MAP_ROLES_COMPOSITE_SCOPE, server); - } - Scope configureScope = authz.getStoreFactory().getScopeStore().findByName(CONFIGURE_SCOPE, server.getId()); - if (configureScope == null) { - configureScope = authz.getStoreFactory().getScopeStore().create(CONFIGURE_SCOPE, server); - } + Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server); + Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server); + Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server); + Scope exchangeFromScope = root.initializeScope(EXCHANGE_FROM_SCOPE, server); + Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server); String resourceName = getResourceName(client); Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId()); @@ -118,6 +131,8 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa scopeset.add(mapRoleScope); scopeset.add(mapRoleClientScope); scopeset.add(mapRoleCompositeScope); + scopeset.add(exchangeFromScope); + scopeset.add(exchangeToScope); resource.updateScopes(scopeset); } String managePermissionName = getManagePermissionName(client); @@ -150,6 +165,16 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa if (mapRoleCompositePermission == null) { Helper.addEmptyScopePermission(authz, server, mapRoleCompositePermissionName, resource, mapRoleCompositeScope); } + String exchangeToPermissionName = getExchangeToPermissionName(client); + Policy exchangeToPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeToPermissionName, server.getId()); + if (exchangeToPermission == null) { + Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope); + } + String exchangeFromPermissionName = getExchangeFromPermissionName(client); + Policy exchangeFromPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeFromPermissionName, server.getId()); + if (exchangeFromPermission == null) { + Helper.addEmptyScopePermission(authz, server, exchangeFromPermissionName, resource, exchangeFromScope); + } } private void deletePolicy(String name, ResourceServer server) { @@ -169,6 +194,8 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa deletePolicy(getMapRolesClientScopePermissionName(client), server); deletePolicy(getMapRolesCompositePermissionName(client), server); deletePolicy(getConfigurePermissionName(client), server); + deletePolicy(getExchangeToPermissionName(client), server); + deletePolicy(getExchangeFromPermissionName(client), server); Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.getId());; if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId()); } @@ -196,6 +223,14 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.MANAGE_SCOPE, server.getId()); } + private Scope exchangeFromScope(ResourceServer server) { + return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_FROM_SCOPE, server.getId()); + } + + private Scope exchangeToScope(ResourceServer server) { + return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId()); + } + private Scope configureScope(ResourceServer server) { return authz.getStoreFactory().getScopeStore().findByName(CONFIGURE_SCOPE, server.getId()); } @@ -271,16 +306,119 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa @Override public Map getPermissions(ClientModel client) { - Map scopes = new HashMap<>(); - scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId()); - scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId()); - scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId()); + initialize(client); + Map scopes = new LinkedHashMap<>(); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission(client).getId()); scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission(client).getId()); scopes.put(CONFIGURE_SCOPE, configurePermission(client).getId()); + scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId()); + scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId()); + scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId()); + scopes.put(EXCHANGE_FROM_SCOPE, exchangeFromPermission(client).getId()); + scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId()); return scopes; } + @Override + public boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from) { + if (!authorizedClient.equals(from)) { + ResourceServer server = resourceServer(from); + if (server == null) { + logger.debug("No resource server set up for target client"); + return false; + } + + Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(from), server.getId()); + if (resource == null) { + logger.debug("No resource object set up for target client"); + return false; + } + + Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(from), server.getId()); + if (policy == null) { + logger.debug("No permission object set up for target client"); + return false; + } + + Set associatedPolicies = policy.getAssociatedPolicies(); + // if no policies attached to permission then just do default behavior + if (associatedPolicies == null || associatedPolicies.isEmpty()) { + logger.debug("No policies set up for permission on target client"); + return false; + } + + Scope scope = exchangeFromScope(server); + if (scope == null) { + logger.debug(EXCHANGE_FROM_SCOPE + " not initialized"); + return false; + } + ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient); + EvaluationContext context = new DefaultEvaluationContext(identity, session) { + @Override + public Map> getBaseAttributes() { + Map> attributes = super.getBaseAttributes(); + attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId())); + return attributes; + } + + }; + return root.evaluatePermission(resource, scope, server, context); + } + return true; + } + + @Override + public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) { + + if (!authorizedClient.equals(to)) { + ResourceServer server = resourceServer(to); + if (server == null) { + logger.debug("No resource server set up for target client"); + return false; + } + + Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(to), server.getId()); + if (resource == null) { + logger.debug("No resource object set up for target client"); + return false; + } + + Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(to), server.getId()); + if (policy == null) { + logger.debug("No permission object set up for target client"); + return false; + } + + Set associatedPolicies = policy.getAssociatedPolicies(); + // if no policies attached to permission then just do default behavior + if (associatedPolicies == null || associatedPolicies.isEmpty()) { + logger.debug("No policies set up for permission on target client"); + return false; + } + + Scope scope = exchangeToScope(server); + if (scope == null) { + logger.debug(EXCHANGE_TO_SCOPE + " not initialized"); + return false; + } + ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient); + EvaluationContext context = new DefaultEvaluationContext(identity, session) { + @Override + public Map> getBaseAttributes() { + Map> attributes = super.getBaseAttributes(); + attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId())); + return attributes; + } + + }; + return root.evaluatePermission(resource, scope, server, context); + } + return true; + } + + + + @Override public boolean canManage(ClientModel client) { @@ -463,6 +601,20 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa return root.evaluatePermission(resource, scope, server); } + @Override + public Policy exchangeFromPermission(ClientModel client) { + ResourceServer server = resourceServer(client); + if (server == null) return null; + return authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(client), server.getId()); + } + + @Override + public Policy exchangeToPermission(ClientModel client) { + ResourceServer server = resourceServer(client); + if (server == null) return null; + return authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(client), server.getId()); + } + @Override public Policy mapRolesPermission(ClientModel client) { ResourceServer server = resourceServer(client); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java index a7e9f374be..722ea1c7a7 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java @@ -31,6 +31,7 @@ import org.keycloak.services.ForbiddenException; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -242,11 +243,12 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag @Override public Map getPermissions(GroupModel group) { - Map scopes = new HashMap<>(); + initialize(group); + Map scopes = new LinkedHashMap<>(); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission(group).getId()); scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission(group).getId()); - scopes.put(MANAGE_MEMBERS_SCOPE, manageMembersPermission(group).getId()); scopes.put(VIEW_MEMBERS_SCOPE, viewMembersPermission(group).getId()); + scopes.put(MANAGE_MEMBERS_SCOPE, manageMembersPermission(group).getId()); scopes.put(MANAGE_MEMBERSHIP_SCOPE, manageMembershipPermission(group).getId()); return scopes; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java index 449530cdf4..fe4a11fe93 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java @@ -277,6 +277,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage return scope; } + public Scope initializeScope(String name, ResourceServer server) { + Scope scope = authz.getStoreFactory().getScopeStore().findByName(name, server.getId()); + if (scope == null) { + scope = authz.getStoreFactory().getScopeStore().create(name, server); + } + return scope; + } + public Scope realmManageScope() { @@ -307,10 +315,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage } public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, Identity identity) { + EvaluationContext context = new DefaultEvaluationContext(identity, session); + return evaluatePermission(resource, scope, resourceServer, context); + } + + public boolean evaluatePermission(Resource resource, Scope scope, ResourceServer resourceServer, EvaluationContext context) { RealmModel oldRealm = session.getContext().getRealm(); try { session.getContext().setRealm(realm); - EvaluationContext context = new DefaultEvaluationContext(identity, session); DecisionResult decisionCollector = new DecisionResult(); List permissions = Permissions.permission(resourceServer, resource, scope); PermissionEvaluator from = authz.evaluators().from(permissions, context); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java index 951e724e7e..0e12861929 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java @@ -36,6 +36,7 @@ import org.keycloak.services.ForbiddenException; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -87,7 +88,8 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme @Override public Map getPermissions(RoleModel role) { - Map scopes = new HashMap<>(); + initialize(role); + Map scopes = new LinkedHashMap<>(); scopes.put(RolePermissionManagement.MAP_ROLE_SCOPE, mapRolePermission(role).getId()); scopes.put(RolePermissionManagement.MAP_ROLE_CLIENT_SCOPE_SCOPE, mapClientScopePermission(role).getId()); scopes.put(RolePermissionManagement.MAP_ROLE_COMPOSITE_SCOPE, mapCompositePermission(role).getId()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java index b1e1b750a4..3ac26ed5fa 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java @@ -34,6 +34,7 @@ import org.keycloak.services.ForbiddenException; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -121,9 +122,10 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme @Override public Map getPermissions() { - Map scopes = new HashMap<>(); - scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission().getId()); + initialize(); + Map scopes = new LinkedHashMap<>(); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission().getId()); + scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission().getId()); scopes.put(MAP_ROLES_SCOPE, mapRolesPermission().getId()); scopes.put(MANAGE_GROUP_MEMBERSHIP_SCOPE, manageGroupMembershipPermission().getId()); scopes.put(IMPERSONATE_SCOPE, adminImpersonatingPermission().getId()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java index ff82166f63..2889118efd 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java @@ -23,6 +23,8 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.TokenVerifier; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -32,6 +34,9 @@ import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; +import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; +import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; @@ -68,7 +73,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest { public static void setupRealm(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName(TEST); - RoleModel realmExchangeable = AdminPermissions.management(session, realm).getRealmManagementClient().addRole(OAuth2Constants.TOKEN_EXCHANGER); RoleModel exampleRole = realm.addRole("example"); @@ -79,48 +83,62 @@ public class TokenExchangeTest extends AbstractKeycloakTest { target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); target.setFullScopeAllowed(false); target.addScopeMapping(exampleRole); - RoleModel targetExchangeable = target.addRole(OAuth2Constants.TOKEN_EXCHANGER); - target = realm.addClient("realm-exchanger"); - target.setClientId("realm-exchanger"); - target.setDirectAccessGrantsEnabled(true); - target.setEnabled(true); - target.setSecret("secret"); - target.setServiceAccountsEnabled(true); - target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - target.setFullScopeAllowed(false); - new org.keycloak.services.managers.ClientManager(new org.keycloak.services.managers.RealmManager(session)).enableServiceAccount(target); - session.users().getServiceAccount(target).grantRole(realmExchangeable); + ClientModel clientExchanger = realm.addClient("client-exchanger"); + clientExchanger.setClientId("client-exchanger"); + clientExchanger.setPublicClient(false); + clientExchanger.setDirectAccessGrantsEnabled(true); + clientExchanger.setEnabled(true); + clientExchanger.setSecret("secret"); + clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + clientExchanger.setFullScopeAllowed(false); - target = realm.addClient("client-exchanger"); - target.setClientId("client-exchanger"); - target.setDirectAccessGrantsEnabled(true); - target.setEnabled(true); - target.setSecret("secret"); - target.setServiceAccountsEnabled(true); - target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - target.setFullScopeAllowed(false); - new org.keycloak.services.managers.ClientManager(new org.keycloak.services.managers.RealmManager(session)).enableServiceAccount(target); - session.users().getServiceAccount(target).grantRole(targetExchangeable); + ClientModel illegal = realm.addClient("illegal"); + illegal.setClientId("illegal"); + illegal.setPublicClient(false); + illegal.setDirectAccessGrantsEnabled(true); + illegal.setEnabled(true); + illegal.setSecret("secret"); + illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + illegal.setFullScopeAllowed(false); - target = realm.addClient("account-not-allowed"); - target.setClientId("account-not-allowed"); - target.setDirectAccessGrantsEnabled(true); - target.setEnabled(true); - target.setSecret("secret"); - target.setServiceAccountsEnabled(true); - target.setFullScopeAllowed(false); - target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - new org.keycloak.services.managers.ClientManager(new org.keycloak.services.managers.RealmManager(session)).enableServiceAccount(target); + ClientModel illegalTo = realm.addClient("illegal-to"); + illegalTo.setClientId("illegal-to"); + illegalTo.setPublicClient(false); + illegalTo.setDirectAccessGrantsEnabled(true); + illegalTo.setEnabled(true); + illegalTo.setSecret("secret"); + illegalTo.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + illegalTo.setFullScopeAllowed(false); + + ClientModel legal = realm.addClient("legal"); + legal.setClientId("legal"); + legal.setPublicClient(false); + legal.setDirectAccessGrantsEnabled(true); + legal.setEnabled(true); + legal.setSecret("secret"); + legal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + legal.setFullScopeAllowed(false); + + AdminPermissionManagement management = AdminPermissions.management(session, realm); + + management.clients().setPermissionsEnabled(target, true); + ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); + clientRep.setName("to"); + clientRep.addClient(clientExchanger.getId()); + clientRep.addClient(legal.getId()); + ResourceServer server = management.realmResourceServer(); + Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server); + management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy); + + management.clients().setPermissionsEnabled(clientExchanger, true); + ClientPolicyRepresentation client2Rep = new ClientPolicyRepresentation(); + client2Rep.setName("from"); + client2Rep.addClient(legal.getId()); + client2Rep.addClient(illegalTo.getId()); + Policy client2Policy = management.authz().getStoreFactory().getPolicyStore().create(client2Rep, server); + management.clients().exchangeFromPermission(clientExchanger).addAssociatedPolicy(client2Policy); - target = realm.addClient("no-account"); - target.setClientId("no-account"); - target.setDirectAccessGrantsEnabled(true); - target.setEnabled(true); - target.setSecret("secret"); - target.setServiceAccountsEnabled(true); - target.setFullScopeAllowed(false); - target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); UserModel user = session.users().addUser(realm, "user"); user.setEnabled(true); @@ -140,18 +158,46 @@ public class TokenExchangeTest extends AbstractKeycloakTest { testingClient.server().run(TokenExchangeTest::setupRealm); oauth.realm(TEST); - oauth.clientId("realm-exchanger"); + oauth.clientId("client-exchanger"); OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password"); String accessToken = response.getAccessToken(); + TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class); + AccessToken token = accessTokenVerifier.parse().getToken(); + Assert.assertEquals(token.getPreferredUsername(), "user"); + Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example")); - response = oauth.doTokenExchange(TEST,accessToken, "target", "realm-exchanger", "secret"); + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret"); - String exchangedTokenString = response.getAccessToken(); - TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); - AccessToken exchangedToken = verifier.parse().getToken(); - Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); - Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "legal", "secret"); + + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("legal", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); + Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret"); + Assert.assertEquals(403, response.getStatusCode()); + } + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal-to", "secret"); + Assert.assertEquals(403, response.getStatusCode()); + } } diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index a2c8d5bbb9..f261105214 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -1340,6 +1340,8 @@ manage-permissions-group.tooltip=Fine grain permssions for admins that want to m manage-authz-group-scope-description=Policies that decide if an admin can manage this group view-authz-group-scope-description=Policies that decide if an admin can view this group view-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group +exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client. +exchange-from-authz-client-scope-description=Policies that decide which clients are allowed to exchange tokens that were generated for this client. manage-authz-client-scope-description=Policies that decide if an admin can manage this client configure-authz-client-scope-description=Reduced management permissions for admin. Cannot set scope, template, or protocol mappers. view-authz-client-scope-description=Policies that decide if an admin can view this client