Evaluate composite roles for hardcoded LDAP roles/groups

Closes: 11771

see also KEYCLOAK-18308
This commit is contained in:
Sven-Torben Janus 2021-05-28 08:36:09 +02:00 committed by Marek Posolda
parent 52ca546cfa
commit 0efa4afd49
10 changed files with 316 additions and 10 deletions

View file

@ -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));
}
};
}

View file

@ -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

View file

@ -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

View file

@ -214,7 +214,9 @@ public class GroupAdapter implements GroupModel.Streams , JpaModel<GroupEntity>
@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<GroupRoleMappingEntity> getGroupRoleMappingEntityTypedQuery(RoleModel role) {

View file

@ -151,7 +151,9 @@ public class MapGroupAdapter extends AbstractGroupModel<MapGroupEntity> {
@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

View file

@ -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
*/

View file

@ -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<configOptions.length ; i+=2) {
String cfgName = configOptions[i];
String cfgValue = configOptions[i+1];
mapperModel.getConfig().putSingle(cfgName, cfgValue);
componentModel.getConfig().putSingle(cfgName, cfgValue);
}
}
@Deprecated
public static void updateGroupMapperConfigOptions(ComponentModel mapperModel, String... configOptions) {
updateConfigOptions(mapperModel, configOptions);
}
// End CRUD model mappers
public static void syncRolesFromLDAP(RealmModel realm, LDAPStorageProvider ldapProvider, ComponentModel providerModel) {

View file

@ -56,7 +56,16 @@ public interface TestingLDAPResource {
void prepareGroupsLDAPTest();
/**
* Prepare groups LDAP tests. Creates some LDAP mappers as well as some built-in GRoups and users in LDAP
* Prepare hardcoded groups 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)
void prepareHardcodedGroupsLDAPTest();
/**
* Prepare groups LDAP tests. Creates some LDAP mappers as well as some built-in Groups and users in LDAP
*/
@POST
@Path("/configure-roles")
@ -64,6 +73,15 @@ public interface TestingLDAPResource {
@Consumes(MediaType.APPLICATION_JSON)
void prepareRolesLDAPTest();
/**
* Prepare hardcoded roles LDAP tests. Creates some LDAP mappers as well as some hardcoded roles and users in LDAP
*/
@POST
@Path("/configure-hardcoded-roles")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
void prepareHardcodedRolesLDAPTest();
/**
* Remove specified user directly just from the LDAP server
*/

View file

@ -0,0 +1,88 @@
/*
* 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.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.testsuite.util.LDAPRule;
import java.io.Serializable;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author sventorben
*/
public class LDAPHardcodedGroupMapperTest extends AbstractLDAPTest implements Serializable {
@ClassRule
public static LDAPRule ldapRule = new LDAPRule();
@Override
protected LDAPRule getLDAPRule() {
return ldapRule;
}
@Override
protected void afterImportTestRealm() {
testingClient.testing().ldap(TEST_REALM_NAME).prepareHardcodedGroupsLDAPTest();
}
/**
* KEYCLOAK-18308
*/
@Test
public void testCompositeGroups() {
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 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));
});
}
}

View file

@ -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));
});
}
}