From 2fa55550f3a09b34938426b4a62cd2c111b41464 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 9 Aug 2017 10:04:14 -0400 Subject: [PATCH] token exchange permissions --- .../java/org/keycloak/OAuth2Constants.java | 1 - .../common/ClientModelIdentity.java | 83 +++++++++ .../keycloak/protocol/oidc/TokenManager.java | 12 ++ .../oidc/endpoints/TokenEndpoint.java | 53 ++---- .../AdminPermissionManagement.java | 2 + .../ClientPermissionManagement.java | 8 + .../admin/permissions/ClientPermissions.java | 168 ++++++++++++++++-- .../admin/permissions/GroupPermissions.java | 1 + .../admin/permissions/MgmtPermissions.java | 14 +- .../admin/permissions/RolePermissions.java | 1 + .../admin/permissions/UserPermissions.java | 1 + .../testsuite/oauth/TokenExchangeTest.java | 138 +++++++++----- 12 files changed, 388 insertions(+), 94 deletions(-) create mode 100644 services/src/main/java/org/keycloak/authorization/common/ClientModelIdentity.java 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 5b70d9b196..0d20597f78 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 @@ -592,45 +592,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); @@ -655,6 +638,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..30381d2f64 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,18 +18,26 @@ 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.Map; @@ -79,6 +87,14 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa return MAP_ROLES_COMPOSITE_SCOPE + ".permission.client." + client.getId(); } + private String getExchangeToPermissionName(ClientModel client) { + return AdminPermissionManagement.EXCHANGE_TO_SCOPE + ".permission.client." + client.getId(); + } + + private String getExchangeFromPermissionName(ClientModel client) { + return AdminPermissionManagement.EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId(); + } + private void initialize(ClientModel client) { ResourceServer server = root.findOrCreateResourceServer(client); Scope manageScope = manageScope(server); @@ -93,18 +109,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(AdminPermissionManagement.EXCHANGE_FROM_SCOPE, server); + Scope exchangeToScope = root.initializeScope(AdminPermissionManagement.EXCHANGE_TO_SCOPE, server); String resourceName = getResourceName(client); Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId()); @@ -118,6 +127,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 +161,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) { @@ -196,6 +217,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(AdminPermissionManagement.EXCHANGE_FROM_SCOPE, server.getId()); + } + + private Scope exchangeToScope(ResourceServer server) { + return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.EXCHANGE_TO_SCOPE, server.getId()); + } + private Scope configureScope(ResourceServer server) { return authz.getStoreFactory().getScopeStore().findByName(CONFIGURE_SCOPE, server.getId()); } @@ -271,6 +300,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa @Override public Map getPermissions(ClientModel client) { + initialize(client); Map scopes = new HashMap<>(); scopes.put(MAP_ROLES_SCOPE, mapRolesPermission(client).getId()); scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId()); @@ -281,6 +311,106 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa 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(AdminPermissionManagement.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(AdminPermissionManagement.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 +593,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..425edb4b81 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 @@ -242,6 +242,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag @Override public Map getPermissions(GroupModel group) { + initialize(group); Map scopes = new HashMap<>(); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission(group).getId()); scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission(group).getId()); 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..33f99db725 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 @@ -87,6 +87,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme @Override public Map getPermissions(RoleModel role) { + initialize(role); Map scopes = new HashMap<>(); scopes.put(RolePermissionManagement.MAP_ROLE_SCOPE, mapRolePermission(role).getId()); scopes.put(RolePermissionManagement.MAP_ROLE_CLIENT_SCOPE_SCOPE, mapClientScopePermission(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..14cf84472b 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 @@ -121,6 +121,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme @Override public Map getPermissions() { + initialize(); Map scopes = new HashMap<>(); scopes.put(AdminPermissionManagement.MANAGE_SCOPE, managePermission().getId()); scopes.put(AdminPermissionManagement.VIEW_SCOPE, viewPermission().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()); + } }