diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPGroupStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPGroupStorageMapper.java index 4aa70c6cf58..0d7226ba7c4 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPGroupStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPGroupStorageMapper.java @@ -21,6 +21,7 @@ import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RoleUtils; import org.keycloak.models.utils.UserModelDelegate; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.idm.model.LDAPObject; @@ -63,7 +64,8 @@ public class HardcodedLDAPGroupStorageMapper extends AbstractLDAPStorageMapper { @Override public boolean isMemberOf(GroupModel group) { - return super.isMemberOf(group) || group.equals(getGroup(realm)); + GroupModel hardcodedGroup = getGroup(realm); + return super.isMemberOf(group) || (hardcodedGroup != null && RoleUtils.isMember(Stream.of(hardcodedGroup), group)); } @Override @@ -74,6 +76,12 @@ public class HardcodedLDAPGroupStorageMapper extends AbstractLDAPStorageMapper { super.leaveGroup(group); } } + + @Override + public boolean hasRole(RoleModel role) { + GroupModel group = getGroup(realm); + return super.hasRole(role) || (group != null && group.hasRole(role)); + } }; } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPRoleStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPRoleStorageMapper.java index 39eee70fb0f..980d0797ac1 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPRoleStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPRoleStorageMapper.java @@ -84,7 +84,8 @@ public class HardcodedLDAPRoleStorageMapper extends AbstractLDAPStorageMapper { @Override public boolean hasRole(RoleModel role) { - return super.hasRole(role) || role.equals(getRole(realm)); + RoleModel hardcodedRole = getRole(realm); + return super.hasRole(role) || (hardcodedRole != null && hardcodedRole.hasRole(role)); } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java index ad3841572ba..0aee782905d 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java @@ -171,8 +171,9 @@ public class GroupAdapter implements GroupModel.Streams { public boolean hasRole(RoleModel role) { if (isUpdated()) return updated.hasRole(role); if (cached.getRoleMappings(modelSupplier).contains(role.getId())) return true; - - return getRoleMappingsStream().anyMatch(r -> r.hasRole(role)); + if (getRoleMappingsStream().anyMatch(r -> r.hasRole(role))) return true; + GroupModel parent = getParent(); + return parent != null && parent.hasRole(role); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java index 7c1d766ee1c..c7ef388d6a9 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java @@ -214,7 +214,9 @@ public class GroupAdapter implements GroupModel.Streams , JpaModel @Override public boolean hasRole(RoleModel role) { - return RoleUtils.hasRole(getRoleMappingsStream(), role); + if (RoleUtils.hasRole(getRoleMappingsStream(), role)) return true; + GroupModel parent = getParent(); + return parent != null && parent.hasRole(role); } protected TypedQuery getGroupRoleMappingEntityTypedQuery(RoleModel role) { diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupAdapter.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupAdapter.java index 3eb1a5cb0be..01a4473aff0 100644 --- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupAdapter.java @@ -151,7 +151,9 @@ public class MapGroupAdapter extends AbstractGroupModel { @Override public boolean hasRole(RoleModel role) { - return RoleUtils.hasRole(getRoleMappingsStream(), role); + if (RoleUtils.hasRole(getRoleMappingsStream(), role)) return true; + GroupModel parent = getParent(); + return parent != null && parent.hasRole(role); } @Override diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java index eda19074351..b662471e923 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java @@ -28,16 +28,21 @@ import javax.ws.rs.QueryParam; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.CacheableStorageProviderModel; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProviderFactory; import org.keycloak.storage.ldap.LDAPUtils; import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapper; +import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapperFactory; import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode; import org.keycloak.storage.ldap.mappers.membership.MembershipType; import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory; @@ -180,7 +185,7 @@ public class TestLDAPResource { } /** - * Prepare groups LDAP tests. Creates some LDAP mappers as well as some built-in GRoups and users in LDAP + * Prepare roles LDAP tests. Creates some LDAP mappers as well as some built-in GRoups and users in LDAP */ @POST @Path("/configure-roles") @@ -225,6 +230,72 @@ public class TestLDAPResource { new RoleLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(realm); } + /** + * Prepare roles LDAP tests. Creates some LDAP mappers as well as some built-in GRoups and users in LDAP + */ + @POST + @Path("/configure-hardcoded-roles") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public void prepareHardcodedRolesLDAPTest() { + ComponentModel ldapCompModel = LDAPTestUtils.getLdapProviderModel(realm); + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapCompModel); + UserStorageProviderModel ldapModel = ldapFedProvider.getModel(); + ldapModel.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE); + ldapModel.setImportEnabled(false); + ldapModel.getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.READ_ONLY.name()); + realm.updateComponent(ldapModel); + + // Add a hardcoded and composite role + RoleModel clientRole = realm.getClientByClientId("admin-cli").addRole("client_role"); + RoleModel hardcodedRole = realm.addRole("hardcoded_role"); + hardcodedRole.addCompositeRole(clientRole); + + // Add role mapper + LDAPTestUtils.addOrUpdateHardcodedRoleMapper(realm, ldapModel); + + // Remove all LDAP users + LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, realm); + + // Add some LDAP users for testing + LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, realm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1"); + } + + /** + * Prepare roles LDAP tests. Creates some LDAP mappers as well as some built-in GRoups and users in LDAP + */ + @POST + @Path("/configure-hardcoded-groups") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public void prepareHardcodedGroupsLDAPTest() { + ComponentModel ldapCompModel = LDAPTestUtils.getLdapProviderModel(realm); + LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapCompModel); + UserStorageProviderModel ldapModel = ldapFedProvider.getModel(); + ldapModel.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE); + ldapModel.setImportEnabled(false); + ldapModel.getConfig().putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.READ_ONLY.name()); + realm.updateComponent(ldapModel); + + // Add a hardcoded group hierarchy with role + RoleModel clientRole = realm.getClientByClientId("admin-cli").addRole("client_role"); + GroupModel parentGroup = realm.createGroup("parent_group"); + parentGroup.grantRole(clientRole); + GroupModel hardcodedGroup = realm.createGroup("hardcoded_group"); + parentGroup.addChild(hardcodedGroup); + + // Add group mapper + LDAPTestUtils.addOrUpdateHardcodedGroupMapper(realm, ldapModel); + + // Remove all LDAP users + LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, realm); + + // Add some LDAP users for testing + LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, realm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234"); + LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1"); + } + /** * Remove specified user directly just from the LDAP server */ diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java index d958766df8e..76653a7c9b5 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/util/LDAPTestUtils.java @@ -34,6 +34,10 @@ import org.keycloak.storage.ldap.idm.model.LDAPDn; import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery; import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore; +import org.keycloak.storage.ldap.mappers.HardcodedLDAPGroupStorageMapper; +import org.keycloak.storage.ldap.mappers.HardcodedLDAPGroupStorageMapperFactory; +import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapper; +import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapperFactory; import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory; @@ -228,6 +232,20 @@ public class LDAPTestUtils { .orElse(null); } + public static void addOrUpdateHardcodedGroupMapper(RealmModel realm, ComponentModel providerModel, String... otherConfigOptions) { + ComponentModel mapperModel = getSubcomponentByName(realm, providerModel, "hardcodedGroupsMapper"); + if (mapperModel != null) { + updateGroupMapperConfigOptions(mapperModel, otherConfigOptions); + realm.updateComponent(mapperModel); + } else { + mapperModel = KeycloakModelUtils.createComponentModel("hardcodedGroupsMapper", providerModel.getId(), + HardcodedLDAPGroupStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(), + HardcodedLDAPGroupStorageMapper.GROUP, "parent_group/hardcoded_group"); + updateConfigOptions(mapperModel, otherConfigOptions); + realm.addComponentModel(mapperModel); + } + } + public static void addOrUpdateGroupMapper(RealmModel realm, ComponentModel providerModel, LDAPGroupMapperMode mode, String descriptionAttrName, String... otherConfigOptions) { ComponentModel mapperModel = getSubcomponentByName(realm, providerModel, "groupsMapper"); if (mapperModel != null) { @@ -247,6 +265,19 @@ public class LDAPTestUtils { } } + public static void addOrUpdateHardcodedRoleMapper(RealmModel realm, ComponentModel providerModel, String... otherConfigOptions) { + ComponentModel mapperModel = getSubcomponentByName(realm, providerModel, "hardcodedRolesMapper"); + if (mapperModel != null) { + updateConfigOptions(mapperModel, otherConfigOptions); + realm.updateComponent(mapperModel); + } else { + mapperModel = KeycloakModelUtils.createComponentModel("hardcodedRolesMapper", providerModel.getId(), HardcodedLDAPRoleStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(), + HardcodedLDAPRoleStorageMapper.ROLE, "hardcoded_role"); + updateConfigOptions(mapperModel, otherConfigOptions); + realm.addComponentModel(mapperModel); + } + } + public static void addOrUpdateRoleMapper(RealmModel realm, ComponentModel providerModel, LDAPGroupMapperMode mode, String... otherConfigOptions) { ComponentModel mapperModel = getSubcomponentByName(realm, providerModel, "rolesMapper"); if (mapperModel != null) { @@ -264,14 +295,19 @@ public class LDAPTestUtils { } } - public static void updateGroupMapperConfigOptions(ComponentModel mapperModel, String... configOptions) { + public static void updateConfigOptions(ComponentModel componentModel, String... configOptions) { for (int i=0 ; i { + LDAPTestContext ctx = LDAPTestContext.init(session); + RealmModel appRealm = ctx.getRealm(); + + // check users + UserModel john = session.users().getUserByUsername(appRealm, "johnkeycloak"); + assertThat(john, notNullValue()); + + // check roles + RoleModel clientRoleGrantedViaHardcodedGroupMembership = appRealm.getClientByClientId("admin-cli").getRole( + "client_role"); + assertThat(clientRoleGrantedViaHardcodedGroupMembership, notNullValue()); + + // check groups + GroupModel hardcodedGroup = appRealm.getGroupsStream() + .filter(it -> it.getName().equals("hardcoded_group")).findFirst().orElse(null); + assertThat(hardcodedGroup, notNullValue()); + GroupModel parentGroup = appRealm.getGroupsStream() + .filter(it -> it.getName().equals("parent_group")).findFirst().orElse(null); + assertThat(parentGroup, notNullValue()); + assertThat(hardcodedGroup.getParent(), equalTo(parentGroup)); + + // check group membership + assertThat(john.isMemberOf(hardcodedGroup), is(true)); + assertThat(john.isMemberOf(parentGroup), is(true)); + + // check role membership + assertThat(john.hasRole(clientRoleGrantedViaHardcodedGroupMembership), is(true)); + }); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPHardcodedRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPHardcodedRoleMapperTest.java new file mode 100644 index 00000000000..c0db20accc3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPHardcodedRoleMapperTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 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.federation.ldap; + +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.testsuite.util.LDAPRule; + +import java.util.stream.Collectors; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +/** + * @author sventorben + */ +public class LDAPHardcodedRoleMapperTest extends AbstractLDAPTest { + + @ClassRule + public static LDAPRule ldapRule = new LDAPRule(); + + @Override + protected LDAPRule getLDAPRule() { + return ldapRule; + } + + @Override + protected void afterImportTestRealm() { + testingClient.testing().ldap(TEST_REALM_NAME).prepareHardcodedRolesLDAPTest(); + } + + /** + * KEYCLOAK-18308 + */ + @Test + public void testCompositeRoles() { + testingClient.server().run(session -> { + LDAPTestContext ctx = LDAPTestContext.init(session); + RealmModel appRealm = ctx.getRealm(); + + // check users + UserModel john = session.users().getUserByUsername(appRealm, "johnkeycloak"); + assertThat(john, notNullValue()); + + // check roles + RoleModel hardcodedRole = appRealm.getRole("hardcoded_role"); + assertThat(hardcodedRole, notNullValue()); + RoleModel compositeClientRole = appRealm.getClientByClientId("admin-cli").getRole("client_role"); + assertThat(compositeClientRole, notNullValue()); + assertThat(hardcodedRole.isComposite(), is(true)); + assertThat(hardcodedRole.getCompositesStream().map(RoleModel::getName).collect(Collectors.toSet()), + containsInAnyOrder("client_role")); + + // check role membership + assertThat(john.hasRole(hardcodedRole), is(true)); + assertThat(john.hasRole(compositeClientRole), is(true)); + }); + } + +}