Merge pull request #1959 from mposolda/master
KEYCLOAK-2154 Added Group mapper for LDAP. LDAP mappers improvements and fixes
This commit is contained in:
commit
5403296ac6
51 changed files with 3473 additions and 860 deletions
|
@ -1,7 +1,9 @@
|
||||||
package org.keycloak.representations.idm;
|
package org.keycloak.representations.idm;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -13,8 +15,9 @@ public class UserFederationMapperTypeRepresentation {
|
||||||
protected String helpText;
|
protected String helpText;
|
||||||
|
|
||||||
protected UserFederationMapperSyncConfigRepresentation syncConfig;
|
protected UserFederationMapperSyncConfigRepresentation syncConfig;
|
||||||
|
|
||||||
protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
|
protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
|
||||||
|
protected Map<String, String> defaultConfig = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
|
@ -63,4 +66,12 @@ public class UserFederationMapperTypeRepresentation {
|
||||||
public void setProperties(List<ConfigPropertyRepresentation> properties) {
|
public void setProperties(List<ConfigPropertyRepresentation> properties) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getDefaultConfig() {
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultConfig(Map<String, String> defaultConfig) {
|
||||||
|
this.defaultConfig = defaultConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,11 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preRemove(RealmModel realm) {
|
public void preRemove(RealmModel realm) {
|
||||||
// complete We don't care about the realm being removed
|
// complete We don't care about the realm being removed
|
||||||
|
|
|
@ -96,6 +96,11 @@ public class KerberosFederationProvider implements UserFederationProvider {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preRemove(RealmModel realm) {
|
public void preRemove(RealmModel realm) {
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.federation.ldap;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
|
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
|
||||||
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
|
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
@ -10,6 +11,8 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilde
|
||||||
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||||
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
|
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
|
||||||
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory;
|
||||||
import org.keycloak.models.CredentialValidationOutput;
|
import org.keycloak.models.CredentialValidationOutput;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -29,6 +32,8 @@ import org.keycloak.common.constants.KerberosConstants;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -185,6 +190,51 @@ public class LDAPFederationProvider implements UserFederationProvider {
|
||||||
return searchResults;
|
return searchResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||||
|
Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId());
|
||||||
|
for (UserFederationMapperModel mapperModel : federationMappers) {
|
||||||
|
LDAPFederationMapper ldapMapper = getMapper(mapperModel);
|
||||||
|
List<UserModel> users = ldapMapper.getGroupMembers(mapperModel, this, realm, group, firstResult, maxResults);
|
||||||
|
|
||||||
|
// Sufficient for now
|
||||||
|
if (users.size() > 0) {
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserModel> loadUsersByLDAPDns(Collection<LDAPDn> userDns, RealmModel realm) {
|
||||||
|
// We have dns of users, who are members of our group. Load them now
|
||||||
|
LDAPQuery query = LDAPUtils.createQueryForUserSearch(this, realm);
|
||||||
|
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||||
|
Condition[] orSubconditions = new Condition[userDns.size()];
|
||||||
|
int index = 0;
|
||||||
|
for (LDAPDn userDn : userDns) {
|
||||||
|
Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue());
|
||||||
|
orSubconditions[index] = condition;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
Condition orCondition = conditionsBuilder.orCondition(orSubconditions);
|
||||||
|
query.addWhereCondition(orCondition);
|
||||||
|
List<LDAPObject> ldapUsers = query.getResultList();
|
||||||
|
|
||||||
|
// We have ldapUsers, Need to load users from KC DB or import them here
|
||||||
|
List<UserModel> result = new LinkedList<>();
|
||||||
|
for (LDAPObject ldapUser : ldapUsers) {
|
||||||
|
String username = LDAPUtils.getUsername(ldapUser, getLdapIdentityStore().getConfig());
|
||||||
|
UserModel kcUser = session.users().getUserByUsername(username, realm);
|
||||||
|
if (!model.getId().equals(kcUser.getFederationLink())) {
|
||||||
|
logger.warnf("Incorrect federation provider of user %s" + kcUser.getUsername());
|
||||||
|
} else {
|
||||||
|
result.add(kcUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
protected List<LDAPObject> searchLDAP(RealmModel realm, Map<String, String> attributes, int maxResults) {
|
protected List<LDAPObject> searchLDAP(RealmModel realm, Map<String, String> attributes, int maxResults) {
|
||||||
|
|
||||||
List<LDAPObject> results = new ArrayList<LDAPObject>();
|
List<LDAPObject> results = new ArrayList<LDAPObject>();
|
||||||
|
|
|
@ -16,6 +16,7 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
|
||||||
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
||||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
|
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
|
||||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
|
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.mappers.UserFederationMapper;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.KeycloakSessionTask;
|
import org.keycloak.models.KeycloakSessionTask;
|
||||||
|
@ -192,6 +193,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
|
public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
|
||||||
|
syncMappers(sessionFactory, realmId, model);
|
||||||
|
|
||||||
logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getDisplayName());
|
logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getDisplayName());
|
||||||
|
|
||||||
LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
|
LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
|
||||||
|
@ -205,6 +208,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserFederationSyncResult syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
|
public UserFederationSyncResult syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
|
||||||
|
syncMappers(sessionFactory, realmId, model);
|
||||||
|
|
||||||
logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, last sync time: " + lastSync, realmId, model.getDisplayName());
|
logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, last sync time: " + lastSync, realmId, model.getDisplayName());
|
||||||
|
|
||||||
// Sync newly created and updated users
|
// Sync newly created and updated users
|
||||||
|
@ -221,6 +226,26 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void syncMappers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
|
||||||
|
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(KeycloakSession session) {
|
||||||
|
LDAPFederationProvider ldapProvider = getInstance(session, model);
|
||||||
|
RealmModel realm = session.realms().getRealm(realmId);
|
||||||
|
Set<UserFederationMapperModel> mappers = realm.getUserFederationMappersByFederationProvider(model.getId());
|
||||||
|
for (UserFederationMapperModel mapperModel : mappers) {
|
||||||
|
UserFederationMapper ldapMapper = session.getProvider(UserFederationMapper.class, mapperModel.getFederationMapperType());
|
||||||
|
UserFederationSyncResult syncResult = ldapMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
if (syncResult.getAdded() > 0 || syncResult.getUpdated() > 0 || syncResult.getRemoved() > 0 || syncResult.getFailed() > 0) {
|
||||||
|
logger.infof("Sync of federation mapper '%s' finished. Status: %s", mapperModel.getName(), syncResult.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) {
|
protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) {
|
||||||
|
|
||||||
final UserFederationSyncResult syncResult = new UserFederationSyncResult();
|
final UserFederationSyncResult syncResult = new UserFederationSyncResult();
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package org.keycloak.federation.ldap;
|
package org.keycloak.federation.ldap;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||||
|
@ -9,6 +12,8 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||||
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||||
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
@ -97,4 +102,109 @@ public class LDAPUtils {
|
||||||
"Mapped UUID LDAP attribute: " + config.getUuidLDAPAttributeName() + ", user DN: " + ldapUser.getDn());
|
"Mapped UUID LDAP attribute: " + config.getUuidLDAPAttributeName() + ", user DN: " + ldapUser.getDn());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// roles & groups
|
||||||
|
|
||||||
|
public static LDAPObject createLDAPGroup(LDAPFederationProvider ldapProvider, String groupName, String groupNameAttribute, Collection<String> objectClasses,
|
||||||
|
String parentDn, Map<String, Set<String>> additionalAttributes) {
|
||||||
|
LDAPObject ldapObject = new LDAPObject();
|
||||||
|
|
||||||
|
ldapObject.setRdnAttributeName(groupNameAttribute);
|
||||||
|
ldapObject.setObjectClasses(objectClasses);
|
||||||
|
ldapObject.setSingleAttribute(groupNameAttribute, groupName);
|
||||||
|
|
||||||
|
LDAPDn roleDn = LDAPDn.fromString(parentDn);
|
||||||
|
roleDn.addFirst(groupNameAttribute, groupName);
|
||||||
|
ldapObject.setDn(roleDn);
|
||||||
|
|
||||||
|
for (Map.Entry<String, Set<String>> attrEntry : additionalAttributes.entrySet()) {
|
||||||
|
ldapObject.setAttribute(attrEntry.getKey(), attrEntry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapProvider.getLdapIdentityStore().add(ldapObject);
|
||||||
|
return ldapObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add ldapChild as member of ldapParent and save ldapParent to LDAP.
|
||||||
|
*
|
||||||
|
* @param ldapProvider
|
||||||
|
* @param membershipType how is 'member' attribute saved (full DN or just uid)
|
||||||
|
* @param memberAttrName usually 'member'
|
||||||
|
* @param ldapParent role or group
|
||||||
|
* @param ldapChild usually user (or child group or child role)
|
||||||
|
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
|
||||||
|
*/
|
||||||
|
public static void addMember(LDAPFederationProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
|
||||||
|
|
||||||
|
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
|
||||||
|
|
||||||
|
// Remove membership placeholder if present
|
||||||
|
if (membershipType == MembershipType.DN) {
|
||||||
|
for (String membership : memberships) {
|
||||||
|
if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
|
||||||
|
memberships.remove(membership);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String membership = getMemberValueOfChildObject(ldapChild, membershipType);
|
||||||
|
|
||||||
|
memberships.add(membership);
|
||||||
|
ldapParent.setAttribute(memberAttrName, memberships);
|
||||||
|
|
||||||
|
if (sendLDAPUpdateRequest) {
|
||||||
|
ldapProvider.getLdapIdentityStore().update(ldapParent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove ldapChild as member of ldapParent and save ldapParent to LDAP.
|
||||||
|
*
|
||||||
|
* @param ldapProvider
|
||||||
|
* @param membershipType how is 'member' attribute saved (full DN or just uid)
|
||||||
|
* @param memberAttrName usually 'member'
|
||||||
|
* @param ldapParent role or group
|
||||||
|
* @param ldapChild usually user (or child group or child role)
|
||||||
|
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
|
||||||
|
*/
|
||||||
|
public static void deleteMember(LDAPFederationProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
|
||||||
|
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
|
||||||
|
|
||||||
|
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType);
|
||||||
|
|
||||||
|
memberships.remove(userMembership);
|
||||||
|
|
||||||
|
// Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here)
|
||||||
|
if (memberships.size() == 0 && membershipType== MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
||||||
|
memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapParent.setAttribute(memberAttrName, memberships);
|
||||||
|
ldapProvider.getLdapIdentityStore().update(ldapParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all existing memberships (values of attribute 'member' ) from the given ldapRole or ldapGroup
|
||||||
|
*
|
||||||
|
* @param memberAttrName usually 'member'
|
||||||
|
* @param ldapRole
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Set<String> getExistingMemberships(String memberAttrName, LDAPObject ldapRole) {
|
||||||
|
Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
|
||||||
|
if (memberships == null) {
|
||||||
|
memberships = new HashSet<>();
|
||||||
|
}
|
||||||
|
return memberships;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value to be used as attribute 'member' in some parent ldapObject
|
||||||
|
*/
|
||||||
|
public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) {
|
||||||
|
return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,20 @@ public class LDAPDn {
|
||||||
return dn;
|
return dn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof LDAPDn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toString().equals(obj.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return toString().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return toString(entries);
|
return toString(entries);
|
||||||
|
|
|
@ -1,32 +1,77 @@
|
||||||
package org.keycloak.federation.ldap.mappers;
|
package org.keycloak.federation.ldap.mappers;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
import org.keycloak.models.UserFederationProvider;
|
import org.keycloak.models.UserFederationProvider;
|
||||||
import org.keycloak.models.UserFederationSyncResult;
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.mappers.UserFederationMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Stateful per-request object
|
||||||
|
*
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapper {
|
public abstract class AbstractLDAPFederationMapper {
|
||||||
|
|
||||||
@Override
|
protected final UserFederationMapperModel mapperModel;
|
||||||
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
protected final LDAPFederationProvider ldapProvider;
|
||||||
throw new IllegalStateException("Not supported");
|
protected final RealmModel realm;
|
||||||
|
|
||||||
|
public AbstractLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||||
|
this.mapperModel = mapperModel;
|
||||||
|
this.ldapProvider = ldapProvider;
|
||||||
|
this.realm = realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
* @see UserFederationMapper#syncDataFromFederationProviderToKeycloak(UserFederationMapperModel, UserFederationProvider, KeycloakSession, RealmModel)
|
||||||
throw new IllegalStateException("Not supported");
|
*/
|
||||||
|
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() {
|
||||||
|
return new UserFederationSyncResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public void close() {
|
* @see UserFederationMapper#syncDataFromKeycloakToFederationProvider(UserFederationMapperModel, UserFederationProvider, KeycloakSession, RealmModel)
|
||||||
|
*/
|
||||||
|
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() {
|
||||||
|
return new UserFederationSyncResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
|
/**
|
||||||
|
* @see LDAPFederationMapper#beforeLDAPQuery(UserFederationMapperModel, LDAPQuery)
|
||||||
|
*/
|
||||||
|
public abstract void beforeLDAPQuery(LDAPQuery query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see LDAPFederationMapper#proxy(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel)
|
||||||
|
*/
|
||||||
|
public abstract UserModel proxy(LDAPObject ldapUser, UserModel delegate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see LDAPFederationMapper#onRegisterUserToLDAP(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel)
|
||||||
|
*/
|
||||||
|
public abstract void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see LDAPFederationMapper#onImportUserFromLDAP(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel, boolean)
|
||||||
|
*/
|
||||||
|
public abstract void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate);
|
||||||
|
|
||||||
|
public List<UserModel> getGroupMembers(GroupModel group, int firstResult, int maxResults) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
|
||||||
String paramm = mapperModel.getConfig().get(paramName);
|
String paramm = mapperModel.getConfig().get(paramName);
|
||||||
return Boolean.parseBoolean(paramm);
|
return Boolean.parseBoolean(paramm);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,16 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
||||||
import org.keycloak.mappers.MapperConfigValidationException;
|
import org.keycloak.mappers.MapperConfigValidationException;
|
||||||
|
import org.keycloak.mappers.UserFederationMapper;
|
||||||
import org.keycloak.mappers.UserFederationMapperFactory;
|
import org.keycloak.mappers.UserFederationMapperFactory;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||||
|
|
||||||
|
@ -23,10 +28,22 @@ public abstract class AbstractLDAPFederationMapperFactory implements UserFederat
|
||||||
// Used to map roles from LDAP to UserModel users
|
// Used to map roles from LDAP to UserModel users
|
||||||
public static final String ROLE_MAPPER_CATEGORY = "Role Mapper";
|
public static final String ROLE_MAPPER_CATEGORY = "Role Mapper";
|
||||||
|
|
||||||
|
|
||||||
|
// Used to map group from LDAP to UserModel users
|
||||||
|
public static final String GROUP_MAPPER_CATEGORY = "Group Mapper";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserFederationMapper create(KeycloakSession session) {
|
||||||
|
return new LDAPFederationMapperBridge(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used just by LDAPFederationMapperBridge.
|
||||||
|
protected abstract AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFederationProviderType() {
|
public String getFederationProviderType() {
|
||||||
return LDAPFederationProviderFactory.PROVIDER_NAME;
|
return LDAPFederationProviderFactory.PROVIDER_NAME;
|
||||||
|
|
|
@ -24,9 +24,13 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
||||||
public static final String LDAP_FULL_NAME_ATTRIBUTE = "ldap.full.name.attribute";
|
public static final String LDAP_FULL_NAME_ATTRIBUTE = "ldap.full.name.attribute";
|
||||||
public static final String READ_ONLY = "read.only";
|
public static final String READ_ONLY = "read.only";
|
||||||
|
|
||||||
|
public FullNameLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||||
|
super(mapperModel, ldapProvider, realm);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||||
String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
|
String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
|
||||||
if (fullName == null) {
|
if (fullName == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -45,19 +49,19 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||||
String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
|
String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
|
||||||
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
|
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
|
||||||
|
|
||||||
if (isReadOnly(mapperModel)) {
|
if (isReadOnly()) {
|
||||||
ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
|
ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
|
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly()) {
|
||||||
|
|
||||||
|
|
||||||
TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
|
TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
|
||||||
|
@ -82,7 +86,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
||||||
|
|
||||||
ensureTransactionStarted();
|
ensureTransactionStarted();
|
||||||
|
|
||||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||||
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
|
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,8 +99,8 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
public void beforeLDAPQuery(LDAPQuery query) {
|
||||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||||
query.addReturningLdapAttribute(ldapFullNameAttrName);
|
query.addReturningLdapAttribute(ldapFullNameAttrName);
|
||||||
|
|
||||||
// Change conditions and compute condition for fullName from the conditions for firstName and lastName. Right now just "equal" condition is supported
|
// Change conditions and compute condition for fullName from the conditions for firstName and lastName. Right now just "equal" condition is supported
|
||||||
|
@ -137,7 +141,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
||||||
query.addWhereCondition(fullNameCondition);
|
query.addWhereCondition(fullNameCondition);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getLdapFullNameAttrName(UserFederationMapperModel mapperModel) {
|
protected String getLdapFullNameAttrName() {
|
||||||
String ldapFullNameAttrName = mapperModel.getConfig().get(LDAP_FULL_NAME_ATTRIBUTE);
|
String ldapFullNameAttrName = mapperModel.getConfig().get(LDAP_FULL_NAME_ATTRIBUTE);
|
||||||
return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName;
|
return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName;
|
||||||
}
|
}
|
||||||
|
@ -154,7 +158,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isReadOnly(UserFederationMapperModel mapperModel) {
|
private boolean isReadOnly() {
|
||||||
return parseBooleanParameter(mapperModel, READ_ONLY);
|
return parseBooleanParameter(mapperModel, READ_ONLY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
package org.keycloak.federation.ldap.mappers;
|
package org.keycloak.federation.ldap.mappers;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPConfig;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
import org.keycloak.mappers.MapperConfigValidationException;
|
import org.keycloak.mappers.MapperConfigValidationException;
|
||||||
import org.keycloak.mappers.UserFederationMapper;
|
import org.keycloak.mappers.UserFederationMapper;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,11 +28,11 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ProviderConfigProperty userModelAttribute = createConfigProperty(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute",
|
ProviderConfigProperty userModelAttribute = createConfigProperty(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute",
|
||||||
"Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN);
|
"Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, null);
|
||||||
configProperties.add(userModelAttribute);
|
configProperties.add(userModelAttribute);
|
||||||
|
|
||||||
ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
|
ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
|
||||||
"For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
"For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||||
configProperties.add(readOnly);
|
configProperties.add(readOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +56,19 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
|
||||||
return configProperties;
|
return configProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||||
|
Map<String, String> defaultValues = new HashMap<>();
|
||||||
|
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||||
|
|
||||||
|
defaultValues.put(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN);
|
||||||
|
|
||||||
|
String readOnly = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? "false" : "true";
|
||||||
|
defaultValues.put(UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
|
||||||
|
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
|
@ -61,7 +80,7 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserFederationMapper create(KeycloakSession session) {
|
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||||
return new FullNameLDAPFederationMapper();
|
return new FullNameLDAPFederationMapper(mapperModel, federationProvider, realm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sufficient if mapper implementation is stateless and doesn't need to "close" any state
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class LDAPFederationMapperBridge implements LDAPFederationMapper {
|
||||||
|
|
||||||
|
private final AbstractLDAPFederationMapperFactory factory;
|
||||||
|
|
||||||
|
public LDAPFederationMapperBridge(AbstractLDAPFederationMapperFactory factory) {
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync groups from LDAP to Keycloak DB
|
||||||
|
@Override
|
||||||
|
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
||||||
|
return getDelegate(mapperModel, federationProvider, realm).syncDataFromFederationProviderToKeycloak();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
||||||
|
return getDelegate(mapperModel, federationProvider, realm).syncDataFromKeycloakToFederationProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||||
|
getDelegate(mapperModel, ldapProvider, realm).onImportUserFromLDAP(ldapUser, user, isCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||||
|
getDelegate(mapperModel, ldapProvider, realm).onRegisterUserToLDAP(ldapUser, localUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
||||||
|
return getDelegate(mapperModel, ldapProvider, realm).proxy(ldapUser, delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||||
|
// Improve if needed
|
||||||
|
getDelegate(mapperModel, null, null).beforeLDAPQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserModel> getGroupMembers(UserFederationMapperModel mapperModel, UserFederationProvider ldapProvider, RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||||
|
return getDelegate(mapperModel, ldapProvider, realm).getGroupMembers(group, firstResult, maxResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AbstractLDAPFederationMapper getDelegate(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm) {
|
||||||
|
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
|
||||||
|
return factory.createMapper(mapperModel, ldapProvider, realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,585 +0,0 @@
|
||||||
package org.keycloak.federation.ldap.mappers;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
|
||||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
|
||||||
import org.keycloak.models.ClientModel;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.LDAPConstants;
|
|
||||||
import org.keycloak.models.ModelException;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.RoleContainerModel;
|
|
||||||
import org.keycloak.models.RoleModel;
|
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
|
||||||
import org.keycloak.models.UserFederationProvider;
|
|
||||||
import org.keycloak.models.UserFederationSyncResult;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
|
||||||
import org.keycloak.models.utils.UserModelDelegate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map realm roles or roles of particular client to LDAP groups
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class);
|
|
||||||
|
|
||||||
// LDAP DN where are roles of this tree saved.
|
|
||||||
public static final String ROLES_DN = "roles.dn";
|
|
||||||
|
|
||||||
// Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn"
|
|
||||||
public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute";
|
|
||||||
|
|
||||||
// Object classes of the role object.
|
|
||||||
public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
|
|
||||||
|
|
||||||
// Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member"
|
|
||||||
public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute";
|
|
||||||
|
|
||||||
// See docs for MembershipType enum
|
|
||||||
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
|
|
||||||
|
|
||||||
// Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID)
|
|
||||||
public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping";
|
|
||||||
|
|
||||||
// ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false
|
|
||||||
public static final String CLIENT_ID = "client.id";
|
|
||||||
|
|
||||||
// See docs for Mode enum
|
|
||||||
public static final String MODE = "mode";
|
|
||||||
|
|
||||||
// See docs for UserRolesRetriever enum
|
|
||||||
public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy";
|
|
||||||
|
|
||||||
// Customized LDAP filter which is added to the whole LDAP query
|
|
||||||
public static final String ROLES_LDAP_FILTER = "roles.ldap.filter";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
|
||||||
Mode mode = getMode(mapperModel);
|
|
||||||
|
|
||||||
// For now, import LDAP role mappings just during create
|
|
||||||
if (mode == Mode.IMPORT && isCreate) {
|
|
||||||
|
|
||||||
List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
|
|
||||||
|
|
||||||
// Import role mappings from LDAP into Keycloak DB
|
|
||||||
String roleNameAttr = getRoleNameLdapAttribute(mapperModel);
|
|
||||||
for (LDAPObject ldapRole : ldapRoles) {
|
|
||||||
String roleName = ldapRole.getAttributeAsString(roleNameAttr);
|
|
||||||
|
|
||||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
RoleModel role = roleContainer.getRole(roleName);
|
|
||||||
|
|
||||||
logger.debugf("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername());
|
|
||||||
user.grantRole(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Sync roles from LDAP to Keycloak DB
|
|
||||||
@Override
|
|
||||||
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
|
||||||
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
|
|
||||||
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getStatus() {
|
|
||||||
return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated());
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
|
||||||
|
|
||||||
// Send LDAP query
|
|
||||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
|
||||||
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
|
||||||
|
|
||||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
|
|
||||||
for (LDAPObject ldapRole : ldapRoles) {
|
|
||||||
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
|
||||||
|
|
||||||
if (roleContainer.getRole(roleName) == null) {
|
|
||||||
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
|
|
||||||
roleContainer.addRole(roleName);
|
|
||||||
syncResult.increaseAdded();
|
|
||||||
} else {
|
|
||||||
syncResult.increaseUpdated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return syncResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Sync roles from Keycloak back to LDAP
|
|
||||||
@Override
|
|
||||||
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
|
||||||
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
|
|
||||||
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getStatus() {
|
|
||||||
return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated());
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
|
||||||
|
|
||||||
// Send LDAP query to see which roles exists there
|
|
||||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
|
||||||
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
|
||||||
|
|
||||||
Set<String> ldapRoleNames = new HashSet<>();
|
|
||||||
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
|
|
||||||
for (LDAPObject ldapRole : ldapRoles) {
|
|
||||||
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
|
||||||
ldapRoleNames.add(roleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
Set<RoleModel> keycloakRoles = roleContainer.getRoles();
|
|
||||||
|
|
||||||
for (RoleModel keycloakRole : keycloakRoles) {
|
|
||||||
String roleName = keycloakRole.getName();
|
|
||||||
if (ldapRoleNames.contains(roleName)) {
|
|
||||||
syncResult.increaseUpdated();
|
|
||||||
} else {
|
|
||||||
logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName);
|
|
||||||
createLDAPRole(mapperModel, roleName, ldapProvider);
|
|
||||||
syncResult.increaseAdded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return syncResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
|
|
||||||
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
|
|
||||||
|
|
||||||
// For now, use same search scope, which is configured "globally" and used for user's search.
|
|
||||||
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
|
|
||||||
|
|
||||||
String rolesDn = getRolesDn(mapperModel);
|
|
||||||
ldapQuery.setSearchDn(rolesDn);
|
|
||||||
|
|
||||||
Collection<String> roleObjectClasses = getRoleObjectClasses(mapperModel, ldapProvider);
|
|
||||||
ldapQuery.addObjectClasses(roleObjectClasses);
|
|
||||||
|
|
||||||
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
|
|
||||||
|
|
||||||
String customFilter = mapperModel.getConfig().get(RoleLDAPFederationMapper.ROLES_LDAP_FILTER);
|
|
||||||
if (customFilter != null && customFilter.trim().length() > 0) {
|
|
||||||
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
|
|
||||||
ldapQuery.addWhereCondition(customFilterCondition);
|
|
||||||
}
|
|
||||||
|
|
||||||
String membershipAttr = getMembershipLdapAttribute(mapperModel);
|
|
||||||
ldapQuery.addReturningLdapAttribute(rolesRdnAttr);
|
|
||||||
ldapQuery.addReturningLdapAttribute(membershipAttr);
|
|
||||||
|
|
||||||
return ldapQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected RoleContainerModel getTargetRoleContainer(UserFederationMapperModel mapperModel, RealmModel realm) {
|
|
||||||
boolean realmRolesMapping = parseBooleanParameter(mapperModel, USE_REALM_ROLES_MAPPING);
|
|
||||||
if (realmRolesMapping) {
|
|
||||||
return realm;
|
|
||||||
} else {
|
|
||||||
String clientId = mapperModel.getConfig().get(CLIENT_ID);
|
|
||||||
if (clientId == null) {
|
|
||||||
throw new ModelException("Using client roles mapping is requested, but parameter client.id not found!");
|
|
||||||
}
|
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
|
||||||
if (client == null) {
|
|
||||||
throw new ModelException("Can't found requested client with clientId: " + clientId);
|
|
||||||
}
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getRolesDn(UserFederationMapperModel mapperModel) {
|
|
||||||
String rolesDn = mapperModel.getConfig().get(ROLES_DN);
|
|
||||||
if (rolesDn == null) {
|
|
||||||
throw new ModelException("Roles DN is null! Check your configuration");
|
|
||||||
}
|
|
||||||
return rolesDn;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getRoleNameLdapAttribute(UserFederationMapperModel mapperModel) {
|
|
||||||
String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE);
|
|
||||||
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getMembershipLdapAttribute(UserFederationMapperModel mapperModel) {
|
|
||||||
String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE);
|
|
||||||
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MembershipType getMembershipTypeLdapAttribute(UserFederationMapperModel mapperModel) {
|
|
||||||
String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE);
|
|
||||||
return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) {
|
|
||||||
return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Collection<String> getRoleObjectClasses(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
|
|
||||||
String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES);
|
|
||||||
if (objectClasses == null) {
|
|
||||||
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
|
|
||||||
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
|
||||||
}
|
|
||||||
String[] objClasses = objectClasses.split(",");
|
|
||||||
|
|
||||||
Set<String> trimmed = new HashSet<>();
|
|
||||||
for (String objectClass : objClasses) {
|
|
||||||
objectClass = objectClass.trim();
|
|
||||||
if (objectClass.length() > 0) {
|
|
||||||
trimmed.add(objectClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mode getMode(UserFederationMapperModel mapperModel) {
|
|
||||||
String modeString = mapperModel.getConfig().get(MODE);
|
|
||||||
if (modeString == null || modeString.isEmpty()) {
|
|
||||||
throw new ModelException("Mode is missing! Check your configuration");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Enum.valueOf(Mode.class, modeString.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
private UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(UserFederationMapperModel mapperModel) {
|
|
||||||
String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY);
|
|
||||||
return (strategyString!=null && !strategyString.isEmpty()) ? Enum.valueOf(UserRolesRetrieveStrategy.class, strategyString) : UserRolesRetrieveStrategy.LOAD_ROLES_BY_MEMBER_ATTRIBUTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LDAPObject createLDAPRole(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider) {
|
|
||||||
LDAPObject ldapObject = new LDAPObject();
|
|
||||||
String roleNameAttribute = getRoleNameLdapAttribute(mapperModel);
|
|
||||||
ldapObject.setRdnAttributeName(roleNameAttribute);
|
|
||||||
ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel, ldapProvider));
|
|
||||||
ldapObject.setSingleAttribute(roleNameAttribute, roleName);
|
|
||||||
|
|
||||||
LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel));
|
|
||||||
roleDn.addFirst(roleNameAttribute, roleName);
|
|
||||||
ldapObject.setDn(roleDn);
|
|
||||||
|
|
||||||
logger.debugf("Creating role [%s] to LDAP with DN [%s]", roleName, roleDn.toString());
|
|
||||||
ldapProvider.getLdapIdentityStore().add(ldapObject);
|
|
||||||
return ldapObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addRoleMappingInLDAP(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
|
||||||
LDAPObject ldapRole = loadLDAPRoleByName(mapperModel, ldapProvider, roleName);
|
|
||||||
if (ldapRole == null) {
|
|
||||||
ldapRole = createLDAPRole(mapperModel, roleName, ldapProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel);
|
|
||||||
|
|
||||||
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
|
|
||||||
|
|
||||||
// Remove membership placeholder if present
|
|
||||||
if (membershipType == MembershipType.DN) {
|
|
||||||
for (String membership : memberships) {
|
|
||||||
if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
|
|
||||||
memberships.remove(membership);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String membership = getMembershipFromUser(ldapUser, membershipType);
|
|
||||||
|
|
||||||
memberships.add(membership);
|
|
||||||
ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
|
|
||||||
|
|
||||||
ldapProvider.getLdapIdentityStore().update(ldapRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteRoleMappingInLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, LDAPObject ldapRole) {
|
|
||||||
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
|
|
||||||
|
|
||||||
MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel);
|
|
||||||
String userMembership = getMembershipFromUser(ldapUser, membershipType);
|
|
||||||
|
|
||||||
memberships.remove(userMembership);
|
|
||||||
|
|
||||||
// Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here)
|
|
||||||
if (memberships.size() == 0 && membershipType==MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
|
||||||
memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
|
|
||||||
ldapProvider.getLdapIdentityStore().update(ldapRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) {
|
|
||||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
|
||||||
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(getRoleNameLdapAttribute(mapperModel), roleName);
|
|
||||||
ldapQuery.addWhereCondition(roleNameCondition);
|
|
||||||
return ldapQuery.getFirstResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
|
|
||||||
String memberAttrName = getMembershipLdapAttribute(mapperModel);
|
|
||||||
Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
|
|
||||||
if (memberships == null) {
|
|
||||||
memberships = new HashSet<>();
|
|
||||||
}
|
|
||||||
return memberships;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
|
||||||
UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel);
|
|
||||||
return strategy.getLDAPRoleMappings(this, mapperModel, ldapProvider, ldapUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
|
||||||
final Mode mode = getMode(mapperModel);
|
|
||||||
|
|
||||||
// For IMPORT mode, all operations are performed against local DB
|
|
||||||
if (mode == Mode.IMPORT) {
|
|
||||||
return delegate;
|
|
||||||
} else {
|
|
||||||
return new LDAPRoleMappingsUserDelegate(delegate, mapperModel, ldapProvider, ldapUser, realm, mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
|
||||||
UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel);
|
|
||||||
strategy.beforeUserLDAPQuery(mapperModel, query);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
|
|
||||||
|
|
||||||
private final UserFederationMapperModel mapperModel;
|
|
||||||
private final LDAPFederationProvider ldapProvider;
|
|
||||||
private final LDAPObject ldapUser;
|
|
||||||
private final RealmModel realm;
|
|
||||||
private final Mode mode;
|
|
||||||
|
|
||||||
// Avoid loading role mappings from LDAP more times per-request
|
|
||||||
private Set<RoleModel> cachedLDAPRoleMappings;
|
|
||||||
|
|
||||||
public LDAPRoleMappingsUserDelegate(UserModel user, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser,
|
|
||||||
RealmModel realm, Mode mode) {
|
|
||||||
super(user);
|
|
||||||
this.mapperModel = mapperModel;
|
|
||||||
this.ldapProvider = ldapProvider;
|
|
||||||
this.ldapUser = ldapUser;
|
|
||||||
this.realm = realm;
|
|
||||||
this.mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<RoleModel> getRealmRoleMappings() {
|
|
||||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
if (roleContainer.equals(realm)) {
|
|
||||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer);
|
|
||||||
|
|
||||||
if (mode == Mode.LDAP_ONLY) {
|
|
||||||
// Use just role mappings from LDAP
|
|
||||||
return ldapRoleMappings;
|
|
||||||
} else {
|
|
||||||
// Merge mappings from both DB and LDAP
|
|
||||||
Set<RoleModel> modelRoleMappings = super.getRealmRoleMappings();
|
|
||||||
ldapRoleMappings.addAll(modelRoleMappings);
|
|
||||||
return ldapRoleMappings;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return super.getRealmRoleMappings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<RoleModel> getClientRoleMappings(ClientModel client) {
|
|
||||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
if (roleContainer.equals(client)) {
|
|
||||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer);
|
|
||||||
|
|
||||||
if (mode == Mode.LDAP_ONLY) {
|
|
||||||
// Use just role mappings from LDAP
|
|
||||||
return ldapRoleMappings;
|
|
||||||
} else {
|
|
||||||
// Merge mappings from both DB and LDAP
|
|
||||||
Set<RoleModel> modelRoleMappings = super.getClientRoleMappings(client);
|
|
||||||
ldapRoleMappings.addAll(modelRoleMappings);
|
|
||||||
return ldapRoleMappings;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return super.getClientRoleMappings(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasRole(RoleModel role) {
|
|
||||||
Set<RoleModel> roles = getRoleMappings();
|
|
||||||
return KeycloakModelUtils.hasRole(roles, role);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void grantRole(RoleModel role) {
|
|
||||||
if (mode == Mode.LDAP_ONLY) {
|
|
||||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
|
|
||||||
if (role.getContainer().equals(roleContainer)) {
|
|
||||||
|
|
||||||
// We need to create new role mappings in LDAP
|
|
||||||
cachedLDAPRoleMappings = null;
|
|
||||||
addRoleMappingInLDAP(mapperModel, role.getName(), ldapProvider, ldapUser);
|
|
||||||
} else {
|
|
||||||
super.grantRole(role);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.grantRole(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<RoleModel> getRoleMappings() {
|
|
||||||
Set<RoleModel> modelRoleMappings = super.getRoleMappings();
|
|
||||||
|
|
||||||
RoleContainerModel targetRoleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, targetRoleContainer);
|
|
||||||
|
|
||||||
if (mode == Mode.LDAP_ONLY) {
|
|
||||||
// For LDAP-only we want to retrieve role mappings of target container just from LDAP
|
|
||||||
Set<RoleModel> modelRolesCopy = new HashSet<>(modelRoleMappings);
|
|
||||||
for (RoleModel role : modelRolesCopy) {
|
|
||||||
if (role.getContainer().equals(targetRoleContainer)) {
|
|
||||||
modelRoleMappings.remove(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modelRoleMappings.addAll(ldapRoleMappings);
|
|
||||||
return modelRoleMappings;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Set<RoleModel> getLDAPRoleMappingsConverted(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, RoleContainerModel roleContainer) {
|
|
||||||
if (cachedLDAPRoleMappings != null) {
|
|
||||||
return new HashSet<>(cachedLDAPRoleMappings);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
|
|
||||||
|
|
||||||
Set<RoleModel> roles = new HashSet<>();
|
|
||||||
String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel);
|
|
||||||
for (LDAPObject role : ldapRoles) {
|
|
||||||
String roleName = role.getAttributeAsString(roleNameLdapAttr);
|
|
||||||
RoleModel modelRole = roleContainer.getRole(roleName);
|
|
||||||
if (modelRole == null) {
|
|
||||||
// Add role to local DB
|
|
||||||
modelRole = roleContainer.addRole(roleName);
|
|
||||||
}
|
|
||||||
roles.add(modelRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedLDAPRoleMappings = new HashSet<>(roles);
|
|
||||||
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteRoleMapping(RoleModel role) {
|
|
||||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
|
||||||
if (role.getContainer().equals(roleContainer)) {
|
|
||||||
|
|
||||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
|
||||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
|
||||||
Condition roleNameCondition = conditionsBuilder.equal(getRoleNameLdapAttribute(mapperModel), role.getName());
|
|
||||||
String membershipUserAttr = getMembershipFromUser(ldapUser, getMembershipTypeLdapAttribute(mapperModel));
|
|
||||||
Condition membershipCondition = conditionsBuilder.equal(getMembershipLdapAttribute(mapperModel), membershipUserAttr);
|
|
||||||
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
|
|
||||||
LDAPObject ldapRole = ldapQuery.getFirstResult();
|
|
||||||
|
|
||||||
if (ldapRole == null) {
|
|
||||||
// Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB.
|
|
||||||
if (mode == Mode.READ_ONLY) {
|
|
||||||
super.deleteRoleMapping(role);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error
|
|
||||||
if (mode == Mode.READ_ONLY) {
|
|
||||||
throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY");
|
|
||||||
} else {
|
|
||||||
// Delete ldap role mappings
|
|
||||||
cachedLDAPRoleMappings = null;
|
|
||||||
deleteRoleMappingInLDAP(mapperModel, ldapProvider, ldapUser, ldapRole);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.deleteRoleMapping(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public enum Mode {
|
|
||||||
/**
|
|
||||||
* All role mappings are retrieved from LDAP and saved into LDAP
|
|
||||||
*/
|
|
||||||
LDAP_ONLY,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then
|
|
||||||
* they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP.
|
|
||||||
* Creating or deleting of role mapping is propagated only to DB.
|
|
||||||
*
|
|
||||||
* This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it
|
|
||||||
* won't be seen by Keycloak
|
|
||||||
*/
|
|
||||||
IMPORT,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB.
|
|
||||||
* Deleting role mappings, which is mapped to LDAP, will throw an error.
|
|
||||||
*/
|
|
||||||
READ_ONLY
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public enum MembershipType {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" )
|
|
||||||
*/
|
|
||||||
DN,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" )
|
|
||||||
*/
|
|
||||||
UID
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -64,9 +64,12 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
||||||
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
|
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
|
||||||
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
|
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
|
||||||
|
|
||||||
|
public UserAttributeLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||||
|
super(mapperModel, ldapProvider, realm);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||||
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||||
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||||
|
|
||||||
|
@ -93,7 +96,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||||
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||||
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||||
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||||
|
@ -130,7 +133,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReadOnly(mapperModel)) {
|
if (isReadOnly()) {
|
||||||
ldapUser.addReadOnlyAttributeName(ldapAttrName);
|
ldapUser.addReadOnlyAttributeName(ldapAttrName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,14 +154,14 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserModel proxy(UserFederationMapperModel mapperModel, final LDAPFederationProvider ldapProvider, final LDAPObject ldapUser, UserModel delegate, final RealmModel realm) {
|
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate) {
|
||||||
final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||||
final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||||
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
|
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
|
||||||
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||||
|
|
||||||
// For writable mode, we want to propagate writing of attribute to LDAP as well
|
// For writable mode, we want to propagate writing of attribute to LDAP as well
|
||||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
|
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly()) {
|
||||||
|
|
||||||
delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
|
delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
|
||||||
|
|
||||||
|
@ -309,13 +312,13 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
public void beforeLDAPQuery(LDAPQuery query) {
|
||||||
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||||
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||||
|
|
||||||
// Add mapped attribute to returning ldap attributes
|
// Add mapped attribute to returning ldap attributes
|
||||||
query.addReturningLdapAttribute(ldapAttrName);
|
query.addReturningLdapAttribute(ldapAttrName);
|
||||||
if (isReadOnly(mapperModel)) {
|
if (isReadOnly()) {
|
||||||
query.addReturningReadOnlyLdapAttribute(ldapAttrName);
|
query.addReturningReadOnlyLdapAttribute(ldapAttrName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,7 +331,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isReadOnly(UserFederationMapperModel mapperModel) {
|
private boolean isReadOnly() {
|
||||||
return parseBooleanParameter(mapperModel, READ_ONLY);
|
return parseBooleanParameter(mapperModel, READ_ONLY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
package org.keycloak.federation.ldap.mappers;
|
package org.keycloak.federation.ldap.mappers;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPConfig;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
import org.keycloak.mappers.MapperConfigValidationException;
|
import org.keycloak.mappers.MapperConfigValidationException;
|
||||||
import org.keycloak.mappers.UserFederationMapper;
|
import org.keycloak.mappers.UserFederationMapper;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,15 +35,15 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
|
||||||
configProperties.add(ldapAttribute);
|
configProperties.add(ldapAttribute);
|
||||||
|
|
||||||
ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
|
ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
|
||||||
"Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
"Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||||
configProperties.add(readOnly);
|
configProperties.add(readOnly);
|
||||||
|
|
||||||
ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always Read Value From LDAP",
|
ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always Read Value From LDAP",
|
||||||
"If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
"If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||||
configProperties.add(alwaysReadValueFromLDAP);
|
configProperties.add(alwaysReadValueFromLDAP);
|
||||||
|
|
||||||
ProviderConfigProperty isMandatoryInLdap = createConfigProperty(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "Is Mandatory In LDAP",
|
ProviderConfigProperty isMandatoryInLdap = createConfigProperty(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "Is Mandatory In LDAP",
|
||||||
"If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
"If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||||
configProperties.add(isMandatoryInLdap);
|
configProperties.add(isMandatoryInLdap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +67,20 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
|
||||||
return configProperties;
|
return configProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||||
|
Map<String, String> defaultValues = new HashMap<>();
|
||||||
|
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||||
|
|
||||||
|
String readOnly = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? "false" : "true";
|
||||||
|
defaultValues.put(UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
|
||||||
|
|
||||||
|
defaultValues.put(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
|
||||||
|
defaultValues.put(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
|
||||||
|
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
|
@ -72,7 +93,7 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserFederationMapper create(KeycloakSession session) {
|
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||||
return new UserAttributeLDAPFederationMapper();
|
return new UserAttributeLDAPFederationMapper(mapperModel, federationProvider, realm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
package org.keycloak.federation.ldap.mappers;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
|
||||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
|
||||||
import org.keycloak.models.LDAPConstants;
|
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strategy for how to retrieve LDAP roles of user
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
public enum UserRolesRetrieveStrategy {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user
|
|
||||||
*/
|
|
||||||
LOAD_ROLES_BY_MEMBER_ATTRIBUTE {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
|
||||||
LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider);
|
|
||||||
String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel);
|
|
||||||
|
|
||||||
String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel));
|
|
||||||
|
|
||||||
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership);
|
|
||||||
ldapQuery.addWhereCondition(membershipCondition);
|
|
||||||
return ldapQuery.getResultList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Roles of user will be retrieved from "memberOf" attribute of our user
|
|
||||||
*/
|
|
||||||
GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
|
||||||
Set<String> memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF);
|
|
||||||
if (memberOfValues == null) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<LDAPObject> roles = new LinkedList<>();
|
|
||||||
LDAPDn parentDn = LDAPDn.fromString(roleMapper.getRolesDn(mapperModel));
|
|
||||||
|
|
||||||
for (String roleDn : memberOfValues) {
|
|
||||||
LDAPDn roleDN = LDAPDn.fromString(roleDn);
|
|
||||||
if (roleDN.isDescendantOf(parentDn)) {
|
|
||||||
LDAPObject role = new LDAPObject();
|
|
||||||
role.setDn(roleDN);
|
|
||||||
|
|
||||||
String firstDN = roleDN.getFirstRdnAttrName();
|
|
||||||
if (firstDN.equalsIgnoreCase(roleMapper.getRoleNameLdapAttribute(mapperModel))) {
|
|
||||||
role.setRdnAttributeName(firstDN);
|
|
||||||
role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue());
|
|
||||||
roles.add(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
|
||||||
query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF);
|
|
||||||
query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user.
|
|
||||||
* The query will be able to retrieve memberships recursively
|
|
||||||
* (Assume "role1" has member "role2" and role2 has member "johnuser". Then searching for roles of "johnuser" will return both "role1" and "role2" )
|
|
||||||
*
|
|
||||||
* This is using AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers
|
|
||||||
*/
|
|
||||||
LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
|
||||||
LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider);
|
|
||||||
String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel);
|
|
||||||
membershipAttr = membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN;
|
|
||||||
String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel));
|
|
||||||
|
|
||||||
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership);
|
|
||||||
ldapQuery.addWhereCondition(membershipCondition);
|
|
||||||
return ldapQuery.getResultList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public abstract List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser);
|
|
||||||
|
|
||||||
public abstract void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper related to mapping of LDAP groups to keycloak model objects (either keycloak roles or keycloak groups)
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface CommonLDAPGroupMapper {
|
||||||
|
|
||||||
|
LDAPQuery createLDAPGroupQuery();
|
||||||
|
|
||||||
|
CommonLDAPGroupMapperConfig getConfig();
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public abstract class CommonLDAPGroupMapperConfig {
|
||||||
|
|
||||||
|
// Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member"
|
||||||
|
public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute";
|
||||||
|
|
||||||
|
// See docs for MembershipType enum
|
||||||
|
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
|
||||||
|
|
||||||
|
// See docs for Mode enum
|
||||||
|
public static final String MODE = "mode";
|
||||||
|
|
||||||
|
// See docs for UserRolesRetriever enum
|
||||||
|
public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy";
|
||||||
|
|
||||||
|
|
||||||
|
protected final UserFederationMapperModel mapperModel;
|
||||||
|
|
||||||
|
public CommonLDAPGroupMapperConfig(UserFederationMapperModel mapperModel) {
|
||||||
|
this.mapperModel = mapperModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMembershipLdapAttribute() {
|
||||||
|
String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE);
|
||||||
|
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MembershipType getMembershipTypeLdapAttribute() {
|
||||||
|
String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE);
|
||||||
|
return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LDAPGroupMapperMode getMode() {
|
||||||
|
String modeString = mapperModel.getConfig().get(MODE);
|
||||||
|
if (modeString == null || modeString.isEmpty()) {
|
||||||
|
throw new ModelException("Mode is missing! Check your configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Enum.valueOf(LDAPGroupMapperMode.class, modeString.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<String> getConfigValues(String str) {
|
||||||
|
String[] objClasses = str.split(",");
|
||||||
|
Set<String> trimmed = new HashSet<>();
|
||||||
|
for (String objectClass : objClasses) {
|
||||||
|
objectClass = objectClass.trim();
|
||||||
|
if (objectClass.length() > 0) {
|
||||||
|
trimmed.add(objectClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String getLDAPGroupsDn();
|
||||||
|
|
||||||
|
public abstract String getLDAPGroupNameLdapAttribute();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public enum LDAPGroupMapperMode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All role mappings are retrieved from LDAP and saved into LDAP
|
||||||
|
*/
|
||||||
|
LDAP_ONLY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then
|
||||||
|
* they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP.
|
||||||
|
* Creating or deleting of role mapping is propagated only to DB.
|
||||||
|
*
|
||||||
|
* This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it
|
||||||
|
* won't be seen by Keycloak
|
||||||
|
*/
|
||||||
|
IMPORT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB.
|
||||||
|
* Deleting role mappings, which is mapped to LDAP, will throw an error.
|
||||||
|
*/
|
||||||
|
READ_ONLY
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public enum MembershipType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" )
|
||||||
|
*/
|
||||||
|
DN,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" )
|
||||||
|
*/
|
||||||
|
UID
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPUtils;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy for how to retrieve LDAP roles of user
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface UserRolesRetrieveStrategy {
|
||||||
|
|
||||||
|
|
||||||
|
List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser);
|
||||||
|
|
||||||
|
void beforeUserLDAPQuery(LDAPQuery query);
|
||||||
|
|
||||||
|
|
||||||
|
// Impl subclasses
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user
|
||||||
|
*/
|
||||||
|
class LoadRolesByMember implements UserRolesRetrieveStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
|
||||||
|
LDAPQuery ldapQuery = roleOrGroupMapper.createLDAPGroupQuery();
|
||||||
|
String membershipAttr = roleOrGroupMapper.getConfig().getMembershipLdapAttribute();
|
||||||
|
|
||||||
|
String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute());
|
||||||
|
|
||||||
|
Condition membershipCondition = getMembershipCondition(membershipAttr, userMembership);
|
||||||
|
ldapQuery.addWhereCondition(membershipCondition);
|
||||||
|
return ldapQuery.getResultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeUserLDAPQuery(LDAPQuery query) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Condition getMembershipCondition(String membershipAttr, String userMembership) {
|
||||||
|
return new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roles of user will be retrieved from "memberOf" attribute of our user
|
||||||
|
*/
|
||||||
|
class GetRolesFromUserMemberOfAttribute implements UserRolesRetrieveStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
|
||||||
|
Set<String> memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF);
|
||||||
|
if (memberOfValues == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LDAPObject> roles = new LinkedList<>();
|
||||||
|
LDAPDn parentDn = LDAPDn.fromString(roleOrGroupMapper.getConfig().getLDAPGroupsDn());
|
||||||
|
|
||||||
|
for (String roleDn : memberOfValues) {
|
||||||
|
LDAPDn roleDN = LDAPDn.fromString(roleDn);
|
||||||
|
if (roleDN.isDescendantOf(parentDn)) {
|
||||||
|
LDAPObject role = new LDAPObject();
|
||||||
|
role.setDn(roleDN);
|
||||||
|
|
||||||
|
String firstDN = roleDN.getFirstRdnAttrName();
|
||||||
|
if (firstDN.equalsIgnoreCase(roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute())) {
|
||||||
|
role.setRdnAttributeName(firstDN);
|
||||||
|
role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue());
|
||||||
|
roles.add(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeUserLDAPQuery(LDAPQuery query) {
|
||||||
|
query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF);
|
||||||
|
query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user.
|
||||||
|
* The query will be able to retrieve memberships recursively with usage of AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers
|
||||||
|
*/
|
||||||
|
class LoadRolesByMemberRecursively extends LoadRolesByMember {
|
||||||
|
|
||||||
|
protected Condition getMembershipCondition(String membershipAttr, String userMembership) {
|
||||||
|
return new LDAPQueryConditionsBuilder().equal(membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN, userMembership);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,634 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.LDAPUtils;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||||
|
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.RoleContainerModel;
|
||||||
|
import org.keycloak.models.RoleModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.models.utils.UserModelDelegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper implements CommonLDAPGroupMapper {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(GroupLDAPFederationMapper.class);
|
||||||
|
|
||||||
|
private final GroupMapperConfig config;
|
||||||
|
private final GroupLDAPFederationMapperFactory factory;
|
||||||
|
|
||||||
|
// Flag to avoid syncing multiple times per transaction
|
||||||
|
private boolean syncFromLDAPPerformedInThisTransaction = false;
|
||||||
|
|
||||||
|
public GroupLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm, GroupLDAPFederationMapperFactory factory) {
|
||||||
|
super(mapperModel, ldapProvider, realm);
|
||||||
|
this.config = new GroupMapperConfig(mapperModel);
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CommonLDAPGroupMapper interface
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LDAPQuery createLDAPGroupQuery() {
|
||||||
|
return createGroupQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonLDAPGroupMapperConfig getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// LDAP Group CRUD operations
|
||||||
|
|
||||||
|
public LDAPQuery createGroupQuery() {
|
||||||
|
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
|
||||||
|
|
||||||
|
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||||
|
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
|
||||||
|
|
||||||
|
String groupsDn = config.getGroupsDn();
|
||||||
|
ldapQuery.setSearchDn(groupsDn);
|
||||||
|
|
||||||
|
Collection<String> groupObjectClasses = config.getGroupObjectClasses(ldapProvider);
|
||||||
|
ldapQuery.addObjectClasses(groupObjectClasses);
|
||||||
|
|
||||||
|
String customFilter = config.getCustomLdapFilter();
|
||||||
|
if (customFilter != null && customFilter.trim().length() > 0) {
|
||||||
|
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
|
||||||
|
ldapQuery.addWhereCondition(customFilterCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapQuery.addReturningLdapAttribute(config.getGroupNameLdapAttribute());
|
||||||
|
ldapQuery.addReturningLdapAttribute(config.getMembershipLdapAttribute());
|
||||||
|
|
||||||
|
for (String groupAttr : config.getGroupAttributes()) {
|
||||||
|
ldapQuery.addReturningLdapAttribute(groupAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ldapQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LDAPObject createLDAPGroup(String groupName, Map<String, Set<String>> additionalAttributes) {
|
||||||
|
LDAPObject ldapGroup = LDAPUtils.createLDAPGroup(ldapProvider, groupName, config.getGroupNameLdapAttribute(), config.getGroupObjectClasses(ldapProvider),
|
||||||
|
config.getGroupsDn(), additionalAttributes);
|
||||||
|
|
||||||
|
logger.debugf("Creating group [%s] to LDAP with DN [%s]", groupName, ldapGroup.getDn().toString());
|
||||||
|
return ldapGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LDAPObject loadLDAPGroupByName(String groupName) {
|
||||||
|
LDAPQuery ldapQuery = createGroupQuery();
|
||||||
|
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getGroupNameLdapAttribute(), groupName);
|
||||||
|
ldapQuery.addWhereCondition(roleNameCondition);
|
||||||
|
return ldapQuery.getFirstResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<LDAPDn> getLDAPSubgroups(LDAPObject ldapGroup) {
|
||||||
|
return getLDAPMembersWithParent(ldapGroup, LDAPDn.fromString(config.getGroupsDn()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get just those members of specified group, which are descendants of "requiredParentDn"
|
||||||
|
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPObject ldapGroup, LDAPDn requiredParentDn) {
|
||||||
|
Set<String> allMemberships = LDAPUtils.getExistingMemberships(config.getMembershipLdapAttribute(), ldapGroup);
|
||||||
|
|
||||||
|
// Filter and keep just groups
|
||||||
|
Set<LDAPDn> result = new HashSet<>();
|
||||||
|
for (String membership : allMemberships) {
|
||||||
|
LDAPDn childDn = LDAPDn.fromString(membership);
|
||||||
|
if (childDn.isDescendantOf(requiredParentDn)) {
|
||||||
|
result.add(childDn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Sync from Ldap to KC
|
||||||
|
|
||||||
|
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() {
|
||||||
|
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStatus() {
|
||||||
|
return String.format("%d imported groups, %d updated groups, %d removed groups", getAdded(), getUpdated(), getRemoved());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debugf("Syncing groups from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||||
|
|
||||||
|
// Get all LDAP groups
|
||||||
|
LDAPQuery ldapQuery = createGroupQuery();
|
||||||
|
List<LDAPObject> ldapGroups = ldapQuery.getResultList();
|
||||||
|
|
||||||
|
// Convert to internal format
|
||||||
|
Map<String, LDAPObject> ldapGroupsMap = new HashMap<>();
|
||||||
|
List<GroupTreeResolver.Group> ldapGroupsRep = new LinkedList<>();
|
||||||
|
|
||||||
|
String groupsRdnAttr = config.getGroupNameLdapAttribute();
|
||||||
|
for (LDAPObject ldapGroup : ldapGroups) {
|
||||||
|
String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr);
|
||||||
|
|
||||||
|
Set<String> subgroupNames = new HashSet<>();
|
||||||
|
for (LDAPDn groupDn : getLDAPSubgroups(ldapGroup)) {
|
||||||
|
subgroupNames.add(groupDn.getFirstRdnAttrValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapGroupsRep.add(new GroupTreeResolver.Group(groupName, subgroupNames));
|
||||||
|
ldapGroupsMap.put(groupName, ldapGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we have list of LDAP groups. Let's form the tree (if needed)
|
||||||
|
if (config.isPreserveGroupsInheritance()) {
|
||||||
|
try {
|
||||||
|
List<GroupTreeResolver.GroupTreeEntry> groupTrees = new GroupTreeResolver().resolveGroupTree(ldapGroupsRep);
|
||||||
|
|
||||||
|
updateKeycloakGroupTree(groupTrees, ldapGroupsMap, syncResult);
|
||||||
|
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||||
|
throw new ModelException("Couldn't resolve groups from LDAP. Fix LDAP or skip preserve inheritance. Details: " + gre.getMessage(), gre);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Set<String> visitedGroupIds = new HashSet<>();
|
||||||
|
|
||||||
|
// Just add flat structure of groups with all groups at top-level
|
||||||
|
for (Map.Entry<String, LDAPObject> groupEntry : ldapGroupsMap.entrySet()) {
|
||||||
|
String groupName = groupEntry.getKey();
|
||||||
|
GroupModel kcExistingGroup = KeycloakModelUtils.findGroupByPath(realm, "/" + groupName);
|
||||||
|
|
||||||
|
if (kcExistingGroup != null) {
|
||||||
|
updateAttributesOfKCGroup(kcExistingGroup, groupEntry.getValue());
|
||||||
|
syncResult.increaseUpdated();
|
||||||
|
visitedGroupIds.add(kcExistingGroup.getId());
|
||||||
|
} else {
|
||||||
|
GroupModel kcGroup = realm.createGroup(groupName);
|
||||||
|
updateAttributesOfKCGroup(kcGroup, groupEntry.getValue());
|
||||||
|
realm.moveGroup(kcGroup, null);
|
||||||
|
syncResult.increaseAdded();
|
||||||
|
visitedGroupIds.add(kcGroup.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possibly remove keycloak groups, which doesn't exists in LDAP
|
||||||
|
if (config.isDropNonExistingGroupsDuringSync()) {
|
||||||
|
dropNonExistingKcGroups(syncResult, visitedGroupIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncFromLDAPPerformedInThisTransaction = true;
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateKeycloakGroupTree(List<GroupTreeResolver.GroupTreeEntry> groupTrees, Map<String, LDAPObject> ldapGroups, UserFederationSyncResult syncResult) {
|
||||||
|
Set<String> visitedGroupIds = new HashSet<>();
|
||||||
|
|
||||||
|
for (GroupTreeResolver.GroupTreeEntry groupEntry : groupTrees) {
|
||||||
|
updateKeycloakGroupTreeEntry(groupEntry, ldapGroups, null, syncResult, visitedGroupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possibly remove keycloak groups, which doesn't exists in LDAP
|
||||||
|
if (config.isDropNonExistingGroupsDuringSync()) {
|
||||||
|
dropNonExistingKcGroups(syncResult, visitedGroupIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateKeycloakGroupTreeEntry(GroupTreeResolver.GroupTreeEntry groupTreeEntry, Map<String, LDAPObject> ldapGroups, GroupModel kcParent, UserFederationSyncResult syncResult, Set<String> visitedGroupIds) {
|
||||||
|
String groupName = groupTreeEntry.getGroupName();
|
||||||
|
|
||||||
|
// Check if group already exists
|
||||||
|
GroupModel kcGroup = null;
|
||||||
|
Collection<GroupModel> subgroups = kcParent == null ? realm.getTopLevelGroups() : kcParent.getSubGroups();
|
||||||
|
for (GroupModel group : subgroups) {
|
||||||
|
if (group.getName().equals(groupName)) {
|
||||||
|
kcGroup = group;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kcGroup != null) {
|
||||||
|
logger.debugf("Updated Keycloak group '%s' from LDAP", kcGroup.getName());
|
||||||
|
updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName()));
|
||||||
|
syncResult.increaseUpdated();
|
||||||
|
} else {
|
||||||
|
kcGroup = realm.createGroup(groupTreeEntry.getGroupName());
|
||||||
|
if (kcParent == null) {
|
||||||
|
realm.moveGroup(kcGroup, null);
|
||||||
|
logger.debugf("Imported top-level group '%s' from LDAP", kcGroup.getName());
|
||||||
|
} else {
|
||||||
|
realm.moveGroup(kcGroup, kcParent);
|
||||||
|
logger.debugf("Imported group '%s' from LDAP as child of group '%s'", kcGroup.getName(), kcParent.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName()));
|
||||||
|
syncResult.increaseAdded();
|
||||||
|
}
|
||||||
|
|
||||||
|
visitedGroupIds.add(kcGroup.getId());
|
||||||
|
|
||||||
|
for (GroupTreeResolver.GroupTreeEntry childEntry : groupTreeEntry.getChildren()) {
|
||||||
|
updateKeycloakGroupTreeEntry(childEntry, ldapGroups, kcGroup, syncResult, visitedGroupIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dropNonExistingKcGroups(UserFederationSyncResult syncResult, Set<String> visitedGroupIds) {
|
||||||
|
// Remove keycloak groups, which doesn't exists in LDAP
|
||||||
|
List<GroupModel> allGroups = realm.getGroups();
|
||||||
|
for (GroupModel kcGroup : allGroups) {
|
||||||
|
if (!visitedGroupIds.contains(kcGroup.getId())) {
|
||||||
|
logger.debugf("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName());
|
||||||
|
realm.removeGroup(kcGroup);
|
||||||
|
syncResult.increaseRemoved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAttributesOfKCGroup(GroupModel kcGroup, LDAPObject ldapGroup) {
|
||||||
|
Collection<String> groupAttributes = config.getGroupAttributes();
|
||||||
|
|
||||||
|
for (String attrName : groupAttributes) {
|
||||||
|
Set<String> attrValues = ldapGroup.getAttributeAsSet(attrName);
|
||||||
|
if (attrValues==null) {
|
||||||
|
kcGroup.removeAttribute(attrName);
|
||||||
|
} else {
|
||||||
|
kcGroup.setAttribute(attrName, new LinkedList<>(attrValues));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override if better effectivity or different algorithm is needed
|
||||||
|
protected GroupModel findKcGroupByLDAPGroup(LDAPObject ldapGroup) {
|
||||||
|
String groupNameAttr = config.getGroupNameLdapAttribute();
|
||||||
|
String groupName = ldapGroup.getAttributeAsString(groupNameAttr);
|
||||||
|
|
||||||
|
List<GroupModel> groups = realm.getGroups();
|
||||||
|
for (GroupModel group : groups) {
|
||||||
|
if (group.getName().equals(groupName)) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GroupModel findKcGroupOrSyncFromLDAP(LDAPObject ldapGroup, UserModel user) {
|
||||||
|
GroupModel kcGroup = findKcGroupByLDAPGroup(ldapGroup);
|
||||||
|
|
||||||
|
if (kcGroup == null) {
|
||||||
|
// Sync groups from LDAP
|
||||||
|
if (!syncFromLDAPPerformedInThisTransaction) {
|
||||||
|
syncDataFromFederationProviderToKeycloak();
|
||||||
|
kcGroup = findKcGroupByLDAPGroup(ldapGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could theoretically happen on some LDAP servers if 'memberof' style is used and 'memberof' attribute of user references non-existing group
|
||||||
|
if (kcGroup == null) {
|
||||||
|
String groupName = ldapGroup.getAttributeAsString(config.getGroupNameLdapAttribute());
|
||||||
|
logger.warnf("User '%s' is member of group '%s', which doesn't exists in LDAP", user.getUsername(), groupName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return kcGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Sync from Keycloak to LDAP
|
||||||
|
|
||||||
|
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() {
|
||||||
|
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStatus() {
|
||||||
|
return String.format("%d groups imported to LDAP, %d groups updated to LDAP, %d groups removed from LDAP", getAdded(), getUpdated(), getRemoved());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString());
|
||||||
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debugf("Syncing groups from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||||
|
|
||||||
|
// Query existing LDAP groups
|
||||||
|
LDAPQuery ldapQuery = createGroupQuery();
|
||||||
|
List<LDAPObject> ldapGroups = ldapQuery.getResultList();
|
||||||
|
|
||||||
|
// Convert them to Map<String, LDAPObject>
|
||||||
|
Map<String, LDAPObject> ldapGroupsMap = new HashMap<>();
|
||||||
|
String groupsRdnAttr = config.getGroupNameLdapAttribute();
|
||||||
|
for (LDAPObject ldapGroup : ldapGroups) {
|
||||||
|
String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr);
|
||||||
|
ldapGroupsMap.put(groupName, ldapGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to track all LDAP groups also exists in Keycloak
|
||||||
|
Set<String> ldapGroupNames = new HashSet<>();
|
||||||
|
|
||||||
|
// Create or update KC groups to LDAP including their attributes
|
||||||
|
for (GroupModel kcGroup : realm.getTopLevelGroups()) {
|
||||||
|
processLdapGroupSyncToLDAP(kcGroup, ldapGroupsMap, ldapGroupNames, syncResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If dropNonExisting, then drop all groups, which doesn't exist in KC from LDAP as well
|
||||||
|
if (config.isDropNonExistingGroupsDuringSync()) {
|
||||||
|
Set<String> copy = new HashSet<>(ldapGroupsMap.keySet());
|
||||||
|
for (String groupName : copy) {
|
||||||
|
if (!ldapGroupNames.contains(groupName)) {
|
||||||
|
LDAPObject ldapGroup = ldapGroupsMap.remove(groupName);
|
||||||
|
ldapProvider.getLdapIdentityStore().remove(ldapGroup);
|
||||||
|
syncResult.increaseRemoved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally process memberships,
|
||||||
|
if (config.isPreserveGroupsInheritance()) {
|
||||||
|
for (GroupModel kcGroup : realm.getTopLevelGroups()) {
|
||||||
|
processLdapGroupMembershipsSyncToLDAP(kcGroup, ldapGroupsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For given kcGroup check if it exists in LDAP (map) by name
|
||||||
|
// If not, create it in LDAP including attributes. Otherwise update attributes in LDAP.
|
||||||
|
// Process this recursively for all subgroups of KC group
|
||||||
|
private void processLdapGroupSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap, Set<String> ldapGroupNames, UserFederationSyncResult syncResult) {
|
||||||
|
String groupName = kcGroup.getName();
|
||||||
|
|
||||||
|
// extract group attributes to be updated to LDAP
|
||||||
|
Map<String, Set<String>> supportedLdapAttributes = new HashMap<>();
|
||||||
|
for (String attrName : config.getGroupAttributes()) {
|
||||||
|
List<String> kcAttrValues = kcGroup.getAttribute(attrName);
|
||||||
|
Set<String> attrValues2 = (kcAttrValues == null || kcAttrValues.isEmpty()) ? null : new HashSet<>(kcAttrValues);
|
||||||
|
supportedLdapAttributes.put(attrName, attrValues2);
|
||||||
|
}
|
||||||
|
|
||||||
|
LDAPObject ldapGroup = ldapGroupsMap.get(groupName);
|
||||||
|
|
||||||
|
if (ldapGroup == null) {
|
||||||
|
ldapGroup = createLDAPGroup(groupName, supportedLdapAttributes);
|
||||||
|
syncResult.increaseAdded();
|
||||||
|
} else {
|
||||||
|
for (Map.Entry<String, Set<String>> attrEntry : supportedLdapAttributes.entrySet()) {
|
||||||
|
ldapGroup.setAttribute(attrEntry.getKey(), attrEntry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapProvider.getLdapIdentityStore().update(ldapGroup);
|
||||||
|
syncResult.increaseUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapGroupsMap.put(groupName, ldapGroup);
|
||||||
|
ldapGroupNames.add(groupName);
|
||||||
|
|
||||||
|
// process KC subgroups
|
||||||
|
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
|
||||||
|
processLdapGroupSyncToLDAP(kcSubgroup, ldapGroupsMap, ldapGroupNames, syncResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync memberships update. Update memberships of group in LDAP based on subgroups from KC. Do it recursively
|
||||||
|
private void processLdapGroupMembershipsSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap) {
|
||||||
|
LDAPObject ldapGroup = ldapGroupsMap.get(kcGroup.getName());
|
||||||
|
Set<LDAPDn> toRemoveSubgroupsDNs = getLDAPSubgroups(ldapGroup);
|
||||||
|
|
||||||
|
// Add LDAP subgroups, which are KC subgroups
|
||||||
|
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
|
||||||
|
for (GroupModel kcSubgroup : kcSubgroups) {
|
||||||
|
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
|
||||||
|
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, ldapSubgroup, false);
|
||||||
|
toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove LDAP subgroups, which are not members in KC anymore
|
||||||
|
for (LDAPDn toRemoveDN : toRemoveSubgroupsDNs) {
|
||||||
|
LDAPObject fakeGroup = new LDAPObject();
|
||||||
|
fakeGroup.setDn(toRemoveDN);
|
||||||
|
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, fakeGroup, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update group to LDAP
|
||||||
|
if (!kcGroup.getSubGroups().isEmpty() || !toRemoveSubgroupsDNs.isEmpty()) {
|
||||||
|
ldapProvider.getLdapIdentityStore().update(ldapGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
|
||||||
|
processLdapGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// group-user membership operations
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserModel> getGroupMembers(GroupModel kcGroup, int firstResult, int maxResults) {
|
||||||
|
LDAPObject ldapGroup = loadLDAPGroupByName(kcGroup.getName());
|
||||||
|
if (ldapGroup == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
||||||
|
Set<LDAPDn> userDns = getLDAPMembersWithParent(ldapGroup, usersDn);
|
||||||
|
|
||||||
|
if (userDns == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userDns.size() <= firstResult) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LDAPDn> dns = new ArrayList<>(userDns);
|
||||||
|
int max = Math.min(dns.size(), firstResult + maxResults);
|
||||||
|
dns = dns.subList(firstResult, max);
|
||||||
|
|
||||||
|
// We have dns of users, who are members of our group. Load them now
|
||||||
|
return ldapProvider.loadUsersByLDAPDns(dns, realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addGroupMappingInLDAP(String groupName, LDAPObject ldapUser) {
|
||||||
|
LDAPObject ldapGroup = loadLDAPGroupByName(groupName);
|
||||||
|
if (ldapGroup == null) {
|
||||||
|
syncDataFromKeycloakToFederationProvider();
|
||||||
|
ldapGroup = loadLDAPGroupByName(groupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) {
|
||||||
|
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<LDAPObject> getLDAPGroupMappings(LDAPObject ldapUser) {
|
||||||
|
String strategyKey = config.getUserGroupsRetrieveStrategy();
|
||||||
|
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
|
||||||
|
return strategy.getLDAPRoleMappings(this, ldapUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void beforeLDAPQuery(LDAPQuery query) {
|
||||||
|
String strategyKey = config.getUserGroupsRetrieveStrategy();
|
||||||
|
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
|
||||||
|
strategy.beforeUserLDAPQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||||
|
final LDAPGroupMapperMode mode = config.getMode();
|
||||||
|
|
||||||
|
// For IMPORT mode, all operations are performed against local DB
|
||||||
|
if (mode == LDAPGroupMapperMode.IMPORT) {
|
||||||
|
return delegate;
|
||||||
|
} else {
|
||||||
|
return new LDAPGroupMappingsUserDelegate(delegate, ldapUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||||
|
LDAPGroupMapperMode mode = config.getMode();
|
||||||
|
|
||||||
|
// For now, import LDAP group mappings just during create
|
||||||
|
if (mode == LDAPGroupMapperMode.IMPORT && isCreate) {
|
||||||
|
|
||||||
|
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
|
||||||
|
|
||||||
|
// Import role mappings from LDAP into Keycloak DB
|
||||||
|
for (LDAPObject ldapGroup : ldapGroups) {
|
||||||
|
|
||||||
|
GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, user);
|
||||||
|
if (kcGroup != null) {
|
||||||
|
logger.debugf("User '%s' joins group '%s' during import from LDAP", user.getUsername(), kcGroup.getName());
|
||||||
|
user.joinGroup(kcGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class LDAPGroupMappingsUserDelegate extends UserModelDelegate {
|
||||||
|
|
||||||
|
private final LDAPObject ldapUser;
|
||||||
|
|
||||||
|
// Avoid loading group mappings from LDAP more times per-request
|
||||||
|
private Set<GroupModel> cachedLDAPGroupMappings;
|
||||||
|
|
||||||
|
public LDAPGroupMappingsUserDelegate(UserModel user, LDAPObject ldapUser) {
|
||||||
|
super(user);
|
||||||
|
this.ldapUser = ldapUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<GroupModel> getGroups() {
|
||||||
|
Set<GroupModel> ldapGroupMappings = getLDAPGroupMappingsConverted();
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
// Use just group mappings from LDAP
|
||||||
|
return ldapGroupMappings;
|
||||||
|
} else {
|
||||||
|
// Merge mappings from both DB and LDAP
|
||||||
|
Set<GroupModel> modelGroupMappings = super.getGroups();
|
||||||
|
ldapGroupMappings.addAll(modelGroupMappings);
|
||||||
|
return ldapGroupMappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void joinGroup(GroupModel group) {
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
// We need to create new role mappings in LDAP
|
||||||
|
cachedLDAPGroupMappings = null;
|
||||||
|
addGroupMappingInLDAP(group.getName(), ldapUser);
|
||||||
|
} else {
|
||||||
|
super.joinGroup(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void leaveGroup(GroupModel group) {
|
||||||
|
LDAPQuery ldapQuery = createGroupQuery();
|
||||||
|
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||||
|
Condition roleNameCondition = conditionsBuilder.equal(config.getGroupNameLdapAttribute(), group.getName());
|
||||||
|
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
|
||||||
|
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
|
||||||
|
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
|
||||||
|
LDAPObject ldapGroup = ldapQuery.getFirstResult();
|
||||||
|
|
||||||
|
if (ldapGroup == null) {
|
||||||
|
// Group mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB.
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||||
|
super.leaveGroup(group);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Group mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||||
|
throw new ModelException("Not possible to delete LDAP group mappings as mapper mode is READ_ONLY");
|
||||||
|
} else {
|
||||||
|
// Delete ldap role mappings
|
||||||
|
cachedLDAPGroupMappings = null;
|
||||||
|
deleteGroupMappingInLDAP(ldapUser, ldapGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMemberOf(GroupModel group) {
|
||||||
|
Set<GroupModel> ldapGroupMappings = getGroups();
|
||||||
|
return ldapGroupMappings.contains(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<GroupModel> getLDAPGroupMappingsConverted() {
|
||||||
|
if (cachedLDAPGroupMappings != null) {
|
||||||
|
return new HashSet<>(cachedLDAPGroupMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
|
||||||
|
|
||||||
|
Set<GroupModel> result = new HashSet<>();
|
||||||
|
for (LDAPObject ldapGroup : ldapGroups) {
|
||||||
|
GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, this);
|
||||||
|
if (kcGroup != null) {
|
||||||
|
result.add(kcGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedLDAPGroupMappings = new HashSet<>(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPConfig;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.role.RoleMapperConfig;
|
||||||
|
import org.keycloak.mappers.MapperConfigValidationException;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "group-ldap-mapper";
|
||||||
|
|
||||||
|
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||||
|
protected static final Map<String, UserRolesRetrieveStrategy> userGroupsStrategies = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// TODO: Merge with RoleLDAPFederationMapperFactory as there are lot of similar properties
|
||||||
|
static {
|
||||||
|
userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
|
||||||
|
userGroupsStrategies.put(GroupMapperConfig.GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
|
||||||
|
userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
|
||||||
|
|
||||||
|
ProviderConfigProperty groupsDn = createConfigProperty(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN",
|
||||||
|
"LDAP DN where are groups of this tree saved. For example 'ou=groups,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null);
|
||||||
|
configProperties.add(groupsDn);
|
||||||
|
|
||||||
|
ProviderConfigProperty groupNameLDAPAttribute = createConfigProperty(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE, "Group Name LDAP Attribute",
|
||||||
|
"Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=Group1,ou=groups,dc=example,dc=org' ",
|
||||||
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
|
configProperties.add(groupNameLDAPAttribute);
|
||||||
|
|
||||||
|
ProviderConfigProperty groupObjectClasses = createConfigProperty(GroupMapperConfig.GROUP_OBJECT_CLASSES, "Group Object Classes",
|
||||||
|
"Object class (or classes) of the group object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
||||||
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
|
configProperties.add(groupObjectClasses);
|
||||||
|
|
||||||
|
ProviderConfigProperty preserveGroupInheritance = createConfigProperty(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "Preserve Group Inheritance",
|
||||||
|
"Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is " +
|
||||||
|
"preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups",
|
||||||
|
ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||||
|
configProperties.add(preserveGroupInheritance);
|
||||||
|
|
||||||
|
ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
|
||||||
|
"Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ",
|
||||||
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
|
configProperties.add(membershipLDAPAttribute);
|
||||||
|
|
||||||
|
List<String> membershipTypes = new LinkedList<>();
|
||||||
|
for (MembershipType membershipType : MembershipType.values()) {
|
||||||
|
membershipTypes.add(membershipType.toString());
|
||||||
|
}
|
||||||
|
ProviderConfigProperty membershipType = createConfigProperty(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type",
|
||||||
|
"DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||||
|
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .",
|
||||||
|
ProviderConfigProperty.LIST_TYPE, membershipTypes);
|
||||||
|
configProperties.add(membershipType);
|
||||||
|
|
||||||
|
ProviderConfigProperty ldapFilter = createConfigProperty(GroupMapperConfig.GROUPS_LDAP_FILTER,
|
||||||
|
"LDAP Filter",
|
||||||
|
"LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'",
|
||||||
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
|
configProperties.add(ldapFilter);
|
||||||
|
|
||||||
|
List<String> modes = new LinkedList<>();
|
||||||
|
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
|
||||||
|
modes.add(mode.toString());
|
||||||
|
}
|
||||||
|
ProviderConfigProperty mode = createConfigProperty(GroupMapperConfig.MODE, "Mode",
|
||||||
|
"LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are " +
|
||||||
|
"retrieved from both LDAP and DB and merged together. New group joins are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where group mappings are " +
|
||||||
|
"retrieved from LDAP just at the time when user is imported from LDAP and then " +
|
||||||
|
"they are saved to local keycloak DB.",
|
||||||
|
ProviderConfigProperty.LIST_TYPE, modes);
|
||||||
|
configProperties.add(mode);
|
||||||
|
|
||||||
|
List<String> roleRetrievers = new LinkedList<>(userGroupsStrategies.keySet());
|
||||||
|
ProviderConfigProperty retriever = createConfigProperty(GroupMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, "User Groups Retrieve Strategy",
|
||||||
|
"Specify how to retrieve groups of user. LOAD_GROUPS_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all groups where 'member' is our user. " +
|
||||||
|
"GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE means that groups of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||||
|
"LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that groups of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension."
|
||||||
|
,
|
||||||
|
ProviderConfigProperty.LIST_TYPE, roleRetrievers);
|
||||||
|
configProperties.add(retriever);
|
||||||
|
|
||||||
|
ProviderConfigProperty mappedGroupAttributes = createConfigProperty(GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES, "Mapped Group Attributes",
|
||||||
|
"List of names of attributes divided by comma. This points to the list of attributes on LDAP group, which will be mapped as attributes of Group in Keycloak. " +
|
||||||
|
"Leave this empty if no additional group attributes are required to be mapped in Keycloak. ",
|
||||||
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
|
configProperties.add(mappedGroupAttributes);
|
||||||
|
|
||||||
|
ProviderConfigProperty dropNonExistingGroupsDuringSync = createConfigProperty(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "Drop non-existing groups during sync",
|
||||||
|
"If this flag is true, then during sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted",
|
||||||
|
ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||||
|
configProperties.add(dropNonExistingGroupsDuringSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Used to map group mappings of groups from some LDAP DN to Keycloak group mappings";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayCategory() {
|
||||||
|
return GROUP_MAPPER_CATEGORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayType() {
|
||||||
|
return "Group mappings";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return configProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||||
|
Map<String, String> defaultValues = new HashMap<>();
|
||||||
|
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||||
|
|
||||||
|
defaultValues.put(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE, LDAPConstants.CN);
|
||||||
|
|
||||||
|
String roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||||
|
defaultValues.put(GroupMapperConfig.GROUP_OBJECT_CLASSES, roleObjectClasses);
|
||||||
|
|
||||||
|
defaultValues.put(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true");
|
||||||
|
defaultValues.put(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, LDAPConstants.MEMBER);
|
||||||
|
|
||||||
|
String mode = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
|
||||||
|
defaultValues.put(GroupMapperConfig.MODE, mode);
|
||||||
|
defaultValues.put(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE);
|
||||||
|
|
||||||
|
defaultValues.put(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "false");
|
||||||
|
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||||
|
return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-groups-to-keycloak", true, "sync-keycloak-groups-to-ldap");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||||
|
checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", mapperModel);
|
||||||
|
checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", mapperModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||||
|
return new GroupLDAPFederationMapper(mapperModel, federationProvider, realm, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserRolesRetrieveStrategy getUserGroupsRetrieveStrategy(String strategyKey) {
|
||||||
|
return userGroupsStrategies.get(strategyKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class GroupMapperConfig extends CommonLDAPGroupMapperConfig {
|
||||||
|
|
||||||
|
// LDAP DN where are groups of this tree saved.
|
||||||
|
public static final String GROUPS_DN = "groups.dn";
|
||||||
|
|
||||||
|
// Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be "cn"
|
||||||
|
public static final String GROUP_NAME_LDAP_ATTRIBUTE = "group.name.ldap.attribute";
|
||||||
|
|
||||||
|
// Object classes of the group object.
|
||||||
|
public static final String GROUP_OBJECT_CLASSES = "group.object.classes";
|
||||||
|
|
||||||
|
// Flag whether group inheritance from LDAP should be propagated to Keycloak group inheritance.
|
||||||
|
public static final String PRESERVE_GROUP_INHERITANCE = "preserve.group.inheritance";
|
||||||
|
|
||||||
|
// Customized LDAP filter which is added to the whole LDAP query
|
||||||
|
public static final String GROUPS_LDAP_FILTER = "groups.ldap.filter";
|
||||||
|
|
||||||
|
// Name of attributes of the LDAP group object, which will be mapped as attributes of Group in Keycloak
|
||||||
|
public static final String MAPPED_GROUP_ATTRIBUTES = "mapped.group.attributes";
|
||||||
|
|
||||||
|
// During sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted
|
||||||
|
public static final String DROP_NON_EXISTING_GROUPS_DURING_SYNC = "drop.non.existing.groups.during.sync";
|
||||||
|
|
||||||
|
// See UserRolesRetrieveStrategy
|
||||||
|
public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE";
|
||||||
|
public static final String GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE";
|
||||||
|
public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY";
|
||||||
|
|
||||||
|
public GroupMapperConfig(UserFederationMapperModel mapperModel) {
|
||||||
|
super(mapperModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getGroupsDn() {
|
||||||
|
String groupsDn = mapperModel.getConfig().get(GROUPS_DN);
|
||||||
|
if (groupsDn == null) {
|
||||||
|
throw new ModelException("Groups DN is null! Check your configuration");
|
||||||
|
}
|
||||||
|
return groupsDn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLDAPGroupsDn() {
|
||||||
|
return getGroupsDn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGroupNameLdapAttribute() {
|
||||||
|
String rolesRdnAttr = mapperModel.getConfig().get(GROUP_NAME_LDAP_ATTRIBUTE);
|
||||||
|
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLDAPGroupNameLdapAttribute() {
|
||||||
|
return getGroupNameLdapAttribute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPreserveGroupsInheritance() {
|
||||||
|
return AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, PRESERVE_GROUP_INHERITANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMembershipLdapAttribute() {
|
||||||
|
String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE);
|
||||||
|
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<String> getGroupObjectClasses(LDAPFederationProvider ldapProvider) {
|
||||||
|
String objectClasses = mapperModel.getConfig().get(GROUP_OBJECT_CLASSES);
|
||||||
|
if (objectClasses == null) {
|
||||||
|
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
|
||||||
|
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getConfigValues(objectClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<String> getGroupAttributes() {
|
||||||
|
String groupAttrs = mapperModel.getConfig().get(MAPPED_GROUP_ATTRIBUTES);
|
||||||
|
return (groupAttrs == null) ? Collections.<String>emptySet() : getConfigValues(groupAttrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomLdapFilter() {
|
||||||
|
return mapperModel.getConfig().get(GROUPS_LDAP_FILTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDropNonExistingGroupsDuringSync() {
|
||||||
|
return AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, DROP_NON_EXISTING_GROUPS_DURING_SYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserGroupsRetrieveStrategy() {
|
||||||
|
String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY);
|
||||||
|
return strategyString!=null ? strategyString : LOAD_GROUPS_BY_MEMBER_ATTRIBUTE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class GroupTreeResolver {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully resolves list of group trees to be used in Keycloak. The input is group info (usually from LDAP) where each "Group" object contains
|
||||||
|
* just it's name and direct children.
|
||||||
|
*
|
||||||
|
* The operation also performs validation as rules for LDAP are less strict than for Keycloak (In LDAP, the recursion is possible and multiple parents of single group is also allowed)
|
||||||
|
*
|
||||||
|
* @param groups
|
||||||
|
* @return
|
||||||
|
* @throws GroupTreeResolveException
|
||||||
|
*/
|
||||||
|
public List<GroupTreeEntry> resolveGroupTree(List<Group> groups) throws GroupTreeResolveException {
|
||||||
|
// 1- Get parents of each group
|
||||||
|
Map<String, List<String>> parentsTree = getParentsTree(groups);
|
||||||
|
|
||||||
|
// 2 - Get rootGroups (groups without parent) and check if there is no group with multiple parents
|
||||||
|
List<String> rootGroups = new LinkedList<>();
|
||||||
|
for (Map.Entry<String, List<String>> group : parentsTree.entrySet()) {
|
||||||
|
int parentCount = group.getValue().size();
|
||||||
|
if (parentCount == 0) {
|
||||||
|
rootGroups.add(group.getKey());
|
||||||
|
} else if (parentCount > 1) {
|
||||||
|
throw new GroupTreeResolveException("Group '" + group.getKey() + "' detected to have multiple parents. This is not allowed in Keycloak. Parents are: " + group.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3 - Just convert to map for easier retrieval
|
||||||
|
Map<String, Group> asMap = new TreeMap<>();
|
||||||
|
for (Group group : groups) {
|
||||||
|
asMap.put(group.getGroupName(), group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4 - Now we have rootGroups. Let's resolve them
|
||||||
|
List<GroupTreeEntry> finalResult = new LinkedList<>();
|
||||||
|
Set<String> visitedGroups = new TreeSet<>();
|
||||||
|
for (String rootGroupName : rootGroups) {
|
||||||
|
List<String> subtree = new LinkedList<>();
|
||||||
|
subtree.add(rootGroupName);
|
||||||
|
GroupTreeEntry groupTree = resolveGroupTree(rootGroupName, asMap, visitedGroups, subtree);
|
||||||
|
finalResult.add(groupTree);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 5 - Check recursion
|
||||||
|
if (visitedGroups.size() != asMap.size()) {
|
||||||
|
// Recursion detected. Try to find where it is
|
||||||
|
for (Map.Entry<String, Group> entry : asMap.entrySet()) {
|
||||||
|
String groupName = entry.getKey();
|
||||||
|
if (!visitedGroups.contains(groupName)) {
|
||||||
|
List<String> subtree = new LinkedList<>();
|
||||||
|
subtree.add(groupName);
|
||||||
|
|
||||||
|
Set<String> newVisitedGroups = new TreeSet<>();
|
||||||
|
resolveGroupTree(groupName, asMap, newVisitedGroups, subtree);
|
||||||
|
visitedGroups.addAll(newVisitedGroups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shouldn't happen
|
||||||
|
throw new GroupTreeResolveException("Illegal state: Recursion detected, but wasn't able to find it");
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<String>> getParentsTree(List<Group> groups) throws GroupTreeResolveException {
|
||||||
|
Map<String, List<String>> result = new TreeMap<>();
|
||||||
|
|
||||||
|
for (Group group : groups) {
|
||||||
|
result.put(group.getGroupName(), new LinkedList<String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Group group : groups) {
|
||||||
|
for (String child : group.getChildrenNames()) {
|
||||||
|
List<String> list = result.get(child);
|
||||||
|
if (list == null) {
|
||||||
|
throw new GroupTreeResolveException("Group '" + child + "' referenced as member of group '" + group.getGroupName() + "' doesn't exists");
|
||||||
|
}
|
||||||
|
list.add(group.getGroupName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupTreeEntry resolveGroupTree(String groupName, Map<String, Group> asMap, Set<String> visitedGroups, List<String> currentSubtree) throws GroupTreeResolveException {
|
||||||
|
if (visitedGroups.contains(groupName)) {
|
||||||
|
throw new GroupTreeResolveException("Recursion detected when trying to resolve group '" + groupName + "'. Whole recursion path: " + currentSubtree);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitedGroups.add(groupName);
|
||||||
|
|
||||||
|
Group group = asMap.get(groupName);
|
||||||
|
|
||||||
|
List<GroupTreeEntry> children = new LinkedList<>();
|
||||||
|
GroupTreeEntry result = new GroupTreeEntry(group.getGroupName(), children);
|
||||||
|
|
||||||
|
for (String childrenName : group.getChildrenNames()) {
|
||||||
|
List<String> subtreeCopy = new LinkedList<>(currentSubtree);
|
||||||
|
subtreeCopy.add(childrenName);
|
||||||
|
GroupTreeEntry childEntry = resolveGroupTree(childrenName, asMap, visitedGroups, subtreeCopy);
|
||||||
|
children.add(childEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// static classes
|
||||||
|
|
||||||
|
public static class GroupTreeResolveException extends Exception {
|
||||||
|
|
||||||
|
public GroupTreeResolveException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class Group {
|
||||||
|
|
||||||
|
private final String groupName;
|
||||||
|
private final List<String> childrenNames;
|
||||||
|
|
||||||
|
public Group(String groupName, String... childrenNames) {
|
||||||
|
this(groupName, Arrays.asList(childrenNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Group(String groupName, Collection<String> childrenNames) {
|
||||||
|
this.groupName = groupName;
|
||||||
|
this.childrenNames = new LinkedList<>(childrenNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGroupName() {
|
||||||
|
return groupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getChildrenNames() {
|
||||||
|
return childrenNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GroupTreeEntry {
|
||||||
|
|
||||||
|
private final String groupName;
|
||||||
|
private final List<GroupTreeEntry> children;
|
||||||
|
|
||||||
|
public GroupTreeEntry(String groupName, List<GroupTreeEntry> children) {
|
||||||
|
this.groupName = groupName;
|
||||||
|
this.children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGroupName() {
|
||||||
|
return groupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GroupTreeEntry> getChildren() {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder("{ " + groupName + " -> [ ");
|
||||||
|
for (GroupTreeEntry child : children) {
|
||||||
|
builder.append(child.toString());
|
||||||
|
}
|
||||||
|
builder.append(" ]}");
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,432 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership.role;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.LDAPUtils;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||||
|
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.RoleContainerModel;
|
||||||
|
import org.keycloak.models.RoleModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.models.utils.UserModelDelegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map realm roles or roles of particular client to LDAP groups
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper implements CommonLDAPGroupMapper {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class);
|
||||||
|
|
||||||
|
private final RoleMapperConfig config;
|
||||||
|
private final RoleLDAPFederationMapperFactory factory;
|
||||||
|
|
||||||
|
public RoleLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm, RoleLDAPFederationMapperFactory factory) {
|
||||||
|
super(mapperModel, ldapProvider, realm);
|
||||||
|
this.config = new RoleMapperConfig(mapperModel);
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LDAPQuery createLDAPGroupQuery() {
|
||||||
|
return createRoleQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonLDAPGroupMapperConfig getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||||
|
LDAPGroupMapperMode mode = config.getMode();
|
||||||
|
|
||||||
|
// For now, import LDAP role mappings just during create
|
||||||
|
if (mode == LDAPGroupMapperMode.IMPORT && isCreate) {
|
||||||
|
|
||||||
|
List<LDAPObject> ldapRoles = getLDAPRoleMappings(ldapUser);
|
||||||
|
|
||||||
|
// Import role mappings from LDAP into Keycloak DB
|
||||||
|
String roleNameAttr = config.getRoleNameLdapAttribute();
|
||||||
|
for (LDAPObject ldapRole : ldapRoles) {
|
||||||
|
String roleName = ldapRole.getAttributeAsString(roleNameAttr);
|
||||||
|
|
||||||
|
RoleContainerModel roleContainer = getTargetRoleContainer();
|
||||||
|
RoleModel role = roleContainer.getRole(roleName);
|
||||||
|
|
||||||
|
if (role == null) {
|
||||||
|
role = roleContainer.addRole(roleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debugf("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername());
|
||||||
|
user.grantRole(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Sync roles from LDAP to Keycloak DB
|
||||||
|
@Override
|
||||||
|
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() {
|
||||||
|
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStatus() {
|
||||||
|
return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||||
|
|
||||||
|
// Send LDAP query
|
||||||
|
LDAPQuery ldapQuery = createRoleQuery();
|
||||||
|
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
||||||
|
|
||||||
|
RoleContainerModel roleContainer = getTargetRoleContainer();
|
||||||
|
String rolesRdnAttr = config.getRoleNameLdapAttribute();
|
||||||
|
for (LDAPObject ldapRole : ldapRoles) {
|
||||||
|
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
||||||
|
|
||||||
|
if (roleContainer.getRole(roleName) == null) {
|
||||||
|
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
|
||||||
|
roleContainer.addRole(roleName);
|
||||||
|
syncResult.increaseAdded();
|
||||||
|
} else {
|
||||||
|
syncResult.increaseUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Sync roles from Keycloak back to LDAP
|
||||||
|
@Override
|
||||||
|
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() {
|
||||||
|
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStatus() {
|
||||||
|
return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString());
|
||||||
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||||
|
|
||||||
|
// Send LDAP query to see which roles exists there
|
||||||
|
LDAPQuery ldapQuery = createRoleQuery();
|
||||||
|
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
||||||
|
|
||||||
|
Set<String> ldapRoleNames = new HashSet<>();
|
||||||
|
String rolesRdnAttr = config.getRoleNameLdapAttribute();
|
||||||
|
for (LDAPObject ldapRole : ldapRoles) {
|
||||||
|
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
||||||
|
ldapRoleNames.add(roleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RoleContainerModel roleContainer = getTargetRoleContainer();
|
||||||
|
Set<RoleModel> keycloakRoles = roleContainer.getRoles();
|
||||||
|
|
||||||
|
for (RoleModel keycloakRole : keycloakRoles) {
|
||||||
|
String roleName = keycloakRole.getName();
|
||||||
|
if (ldapRoleNames.contains(roleName)) {
|
||||||
|
syncResult.increaseUpdated();
|
||||||
|
} else {
|
||||||
|
logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName);
|
||||||
|
createLDAPRole(roleName);
|
||||||
|
syncResult.increaseAdded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Possible to merge with GroupMapper and move to common class
|
||||||
|
public LDAPQuery createRoleQuery() {
|
||||||
|
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
|
||||||
|
|
||||||
|
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||||
|
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
|
||||||
|
|
||||||
|
String rolesDn = config.getRolesDn();
|
||||||
|
ldapQuery.setSearchDn(rolesDn);
|
||||||
|
|
||||||
|
Collection<String> roleObjectClasses = config.getRoleObjectClasses(ldapProvider);
|
||||||
|
ldapQuery.addObjectClasses(roleObjectClasses);
|
||||||
|
|
||||||
|
String rolesRdnAttr = config.getRoleNameLdapAttribute();
|
||||||
|
|
||||||
|
String customFilter = config.getCustomLdapFilter();
|
||||||
|
if (customFilter != null && customFilter.trim().length() > 0) {
|
||||||
|
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
|
||||||
|
ldapQuery.addWhereCondition(customFilterCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
String membershipAttr = config.getMembershipLdapAttribute();
|
||||||
|
ldapQuery.addReturningLdapAttribute(rolesRdnAttr);
|
||||||
|
ldapQuery.addReturningLdapAttribute(membershipAttr);
|
||||||
|
|
||||||
|
return ldapQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected RoleContainerModel getTargetRoleContainer() {
|
||||||
|
boolean realmRolesMapping = config.isRealmRolesMapping();
|
||||||
|
if (realmRolesMapping) {
|
||||||
|
return realm;
|
||||||
|
} else {
|
||||||
|
String clientId = config.getClientId();
|
||||||
|
if (clientId == null) {
|
||||||
|
throw new ModelException("Using client roles mapping is requested, but parameter client.id not found!");
|
||||||
|
}
|
||||||
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
|
if (client == null) {
|
||||||
|
throw new ModelException("Can't found requested client with clientId: " + clientId);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public LDAPObject createLDAPRole(String roleName) {
|
||||||
|
LDAPObject ldapRole = LDAPUtils.createLDAPGroup(ldapProvider, roleName, config.getRoleNameLdapAttribute(), config.getRoleObjectClasses(ldapProvider),
|
||||||
|
config.getRolesDn(), Collections.<String, Set<String>>emptyMap());
|
||||||
|
|
||||||
|
logger.debugf("Creating role [%s] to LDAP with DN [%s]", roleName, ldapRole.getDn().toString());
|
||||||
|
return ldapRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRoleMappingInLDAP(String roleName, LDAPObject ldapUser) {
|
||||||
|
LDAPObject ldapRole = loadLDAPRoleByName(roleName);
|
||||||
|
if (ldapRole == null) {
|
||||||
|
ldapRole = createLDAPRole(roleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) {
|
||||||
|
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LDAPObject loadLDAPRoleByName(String roleName) {
|
||||||
|
LDAPQuery ldapQuery = createRoleQuery();
|
||||||
|
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getRoleNameLdapAttribute(), roleName);
|
||||||
|
ldapQuery.addWhereCondition(roleNameCondition);
|
||||||
|
return ldapQuery.getFirstResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<LDAPObject> getLDAPRoleMappings(LDAPObject ldapUser) {
|
||||||
|
String strategyKey = config.getUserRolesRetrieveStrategy();
|
||||||
|
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
|
||||||
|
return strategy.getLDAPRoleMappings(this, ldapUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||||
|
final LDAPGroupMapperMode mode = config.getMode();
|
||||||
|
|
||||||
|
// For IMPORT mode, all operations are performed against local DB
|
||||||
|
if (mode == LDAPGroupMapperMode.IMPORT) {
|
||||||
|
return delegate;
|
||||||
|
} else {
|
||||||
|
return new LDAPRoleMappingsUserDelegate(delegate, ldapUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeLDAPQuery(LDAPQuery query) {
|
||||||
|
String strategyKey = config.getUserRolesRetrieveStrategy();
|
||||||
|
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
|
||||||
|
strategy.beforeUserLDAPQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
|
||||||
|
|
||||||
|
private final LDAPObject ldapUser;
|
||||||
|
private final RoleContainerModel roleContainer;
|
||||||
|
|
||||||
|
// Avoid loading role mappings from LDAP more times per-request
|
||||||
|
private Set<RoleModel> cachedLDAPRoleMappings;
|
||||||
|
|
||||||
|
public LDAPRoleMappingsUserDelegate(UserModel user, LDAPObject ldapUser) {
|
||||||
|
super(user);
|
||||||
|
this.ldapUser = ldapUser;
|
||||||
|
this.roleContainer = getTargetRoleContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<RoleModel> getRealmRoleMappings() {
|
||||||
|
if (roleContainer.equals(realm)) {
|
||||||
|
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
|
||||||
|
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
// Use just role mappings from LDAP
|
||||||
|
return ldapRoleMappings;
|
||||||
|
} else {
|
||||||
|
// Merge mappings from both DB and LDAP
|
||||||
|
Set<RoleModel> modelRoleMappings = super.getRealmRoleMappings();
|
||||||
|
ldapRoleMappings.addAll(modelRoleMappings);
|
||||||
|
return ldapRoleMappings;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return super.getRealmRoleMappings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<RoleModel> getClientRoleMappings(ClientModel client) {
|
||||||
|
if (roleContainer.equals(client)) {
|
||||||
|
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
|
||||||
|
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
// Use just role mappings from LDAP
|
||||||
|
return ldapRoleMappings;
|
||||||
|
} else {
|
||||||
|
// Merge mappings from both DB and LDAP
|
||||||
|
Set<RoleModel> modelRoleMappings = super.getClientRoleMappings(client);
|
||||||
|
ldapRoleMappings.addAll(modelRoleMappings);
|
||||||
|
return ldapRoleMappings;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return super.getClientRoleMappings(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasRole(RoleModel role) {
|
||||||
|
Set<RoleModel> roles = getRoleMappings();
|
||||||
|
return KeycloakModelUtils.hasRole(roles, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void grantRole(RoleModel role) {
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
|
||||||
|
if (role.getContainer().equals(roleContainer)) {
|
||||||
|
|
||||||
|
// We need to create new role mappings in LDAP
|
||||||
|
cachedLDAPRoleMappings = null;
|
||||||
|
addRoleMappingInLDAP(role.getName(), ldapUser);
|
||||||
|
} else {
|
||||||
|
super.grantRole(role);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.grantRole(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<RoleModel> getRoleMappings() {
|
||||||
|
Set<RoleModel> modelRoleMappings = super.getRoleMappings();
|
||||||
|
|
||||||
|
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
|
||||||
|
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||||
|
// For LDAP-only we want to retrieve role mappings of target container just from LDAP
|
||||||
|
Set<RoleModel> modelRolesCopy = new HashSet<>(modelRoleMappings);
|
||||||
|
for (RoleModel role : modelRolesCopy) {
|
||||||
|
if (role.getContainer().equals(roleContainer)) {
|
||||||
|
modelRoleMappings.remove(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelRoleMappings.addAll(ldapRoleMappings);
|
||||||
|
return modelRoleMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<RoleModel> getLDAPRoleMappingsConverted() {
|
||||||
|
if (cachedLDAPRoleMappings != null) {
|
||||||
|
return new HashSet<>(cachedLDAPRoleMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LDAPObject> ldapRoles = getLDAPRoleMappings(ldapUser);
|
||||||
|
|
||||||
|
Set<RoleModel> roles = new HashSet<>();
|
||||||
|
String roleNameLdapAttr = config.getRoleNameLdapAttribute();
|
||||||
|
for (LDAPObject role : ldapRoles) {
|
||||||
|
String roleName = role.getAttributeAsString(roleNameLdapAttr);
|
||||||
|
RoleModel modelRole = roleContainer.getRole(roleName);
|
||||||
|
if (modelRole == null) {
|
||||||
|
// Add role to local DB
|
||||||
|
modelRole = roleContainer.addRole(roleName);
|
||||||
|
}
|
||||||
|
roles.add(modelRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedLDAPRoleMappings = new HashSet<>(roles);
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteRoleMapping(RoleModel role) {
|
||||||
|
if (role.getContainer().equals(roleContainer)) {
|
||||||
|
|
||||||
|
LDAPQuery ldapQuery = createRoleQuery();
|
||||||
|
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||||
|
Condition roleNameCondition = conditionsBuilder.equal(config.getRoleNameLdapAttribute(), role.getName());
|
||||||
|
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
|
||||||
|
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
|
||||||
|
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
|
||||||
|
LDAPObject ldapRole = ldapQuery.getFirstResult();
|
||||||
|
|
||||||
|
if (ldapRole == null) {
|
||||||
|
// Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB.
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||||
|
super.deleteRoleMapping(role);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error
|
||||||
|
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||||
|
throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY");
|
||||||
|
} else {
|
||||||
|
// Delete ldap role mappings
|
||||||
|
cachedLDAPRoleMappings = null;
|
||||||
|
deleteRoleMappingInLDAP(ldapUser, ldapRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.deleteRoleMapping(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,15 +1,25 @@
|
||||||
package org.keycloak.federation.ldap.mappers;
|
package org.keycloak.federation.ldap.mappers.membership.role;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.keycloak.federation.ldap.LDAPConfig;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||||
import org.keycloak.mappers.MapperConfigValidationException;
|
import org.keycloak.mappers.MapperConfigValidationException;
|
||||||
import org.keycloak.mappers.UserFederationMapper;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||||
|
|
||||||
|
@ -18,45 +28,49 @@ import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresenta
|
||||||
*/
|
*/
|
||||||
public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
|
public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapperFactory.class);
|
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "role-ldap-mapper";
|
public static final String PROVIDER_ID = "role-ldap-mapper";
|
||||||
|
|
||||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||||
|
protected static final Map<String, UserRolesRetrieveStrategy> userRolesStrategies = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ProviderConfigProperty rolesDn = createConfigProperty(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN",
|
userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
|
||||||
|
userRolesStrategies.put(RoleMapperConfig.GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
|
||||||
|
userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
|
||||||
|
|
||||||
|
ProviderConfigProperty rolesDn = createConfigProperty(RoleMapperConfig.ROLES_DN, "LDAP Roles DN",
|
||||||
"LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null);
|
"LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null);
|
||||||
configProperties.add(rolesDn);
|
configProperties.add(rolesDn);
|
||||||
|
|
||||||
ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute",
|
ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute",
|
||||||
"Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ",
|
"Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ",
|
||||||
ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN);
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
configProperties.add(roleNameLDAPAttribute);
|
configProperties.add(roleNameLDAPAttribute);
|
||||||
|
|
||||||
ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes",
|
ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleMapperConfig.ROLE_OBJECT_CLASSES, "Role Object Classes",
|
||||||
"Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
"Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
||||||
ProviderConfigProperty.STRING_TYPE, null);
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
configProperties.add(roleObjectClasses);
|
configProperties.add(roleObjectClasses);
|
||||||
|
|
||||||
ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
|
ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
|
||||||
"Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ",
|
"Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ",
|
||||||
ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER);
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
configProperties.add(membershipLDAPAttribute);
|
configProperties.add(membershipLDAPAttribute);
|
||||||
|
|
||||||
|
|
||||||
List<String> membershipTypes = new LinkedList<>();
|
List<String> membershipTypes = new LinkedList<>();
|
||||||
for (RoleLDAPFederationMapper.MembershipType membershipType : RoleLDAPFederationMapper.MembershipType.values()) {
|
for (MembershipType membershipType : MembershipType.values()) {
|
||||||
membershipTypes.add(membershipType.toString());
|
membershipTypes.add(membershipType.toString());
|
||||||
}
|
}
|
||||||
ProviderConfigProperty membershipType = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type",
|
ProviderConfigProperty membershipType = createConfigProperty(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type",
|
||||||
"DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
"DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||||
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .",
|
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .",
|
||||||
ProviderConfigProperty.LIST_TYPE, membershipTypes);
|
ProviderConfigProperty.LIST_TYPE, membershipTypes);
|
||||||
configProperties.add(membershipType);
|
configProperties.add(membershipType);
|
||||||
|
|
||||||
|
|
||||||
ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER,
|
ProviderConfigProperty ldapFilter = createConfigProperty(RoleMapperConfig.ROLES_LDAP_FILTER,
|
||||||
"LDAP Filter",
|
"LDAP Filter",
|
||||||
"LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'",
|
"LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'",
|
||||||
ProviderConfigProperty.STRING_TYPE, null);
|
ProviderConfigProperty.STRING_TYPE, null);
|
||||||
|
@ -64,10 +78,10 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
||||||
|
|
||||||
|
|
||||||
List<String> modes = new LinkedList<>();
|
List<String> modes = new LinkedList<>();
|
||||||
for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) {
|
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
|
||||||
modes.add(mode.toString());
|
modes.add(mode.toString());
|
||||||
}
|
}
|
||||||
ProviderConfigProperty mode = createConfigProperty(RoleLDAPFederationMapper.MODE, "Mode",
|
ProviderConfigProperty mode = createConfigProperty(RoleMapperConfig.MODE, "Mode",
|
||||||
"LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " +
|
"LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " +
|
||||||
"retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " +
|
"retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " +
|
||||||
"they are saved to local keycloak DB.",
|
"they are saved to local keycloak DB.",
|
||||||
|
@ -75,11 +89,8 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
||||||
configProperties.add(mode);
|
configProperties.add(mode);
|
||||||
|
|
||||||
|
|
||||||
List<String> roleRetrievers = new LinkedList<>();
|
List<String> roleRetrievers = new LinkedList<>(userRolesStrategies.keySet());
|
||||||
for (UserRolesRetrieveStrategy retriever : UserRolesRetrieveStrategy.values()) {
|
ProviderConfigProperty retriever = createConfigProperty(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy",
|
||||||
roleRetrievers.add(retriever.toString());
|
|
||||||
}
|
|
||||||
ProviderConfigProperty retriever = createConfigProperty(RoleLDAPFederationMapper.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy",
|
|
||||||
"Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
|
"Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
|
||||||
"GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
|
"GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||||
"LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension."
|
"LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension."
|
||||||
|
@ -88,11 +99,11 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
||||||
configProperties.add(retriever);
|
configProperties.add(retriever);
|
||||||
|
|
||||||
|
|
||||||
ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping",
|
ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleMapperConfig.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping",
|
||||||
"If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, "true");
|
"If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||||
configProperties.add(useRealmRolesMappings);
|
configProperties.add(useRealmRolesMappings);
|
||||||
|
|
||||||
ProviderConfigProperty clientIdProperty = createConfigProperty(RoleLDAPFederationMapper.CLIENT_ID, "Client ID",
|
ProviderConfigProperty clientIdProperty = createConfigProperty(RoleMapperConfig.CLIENT_ID, "Client ID",
|
||||||
"Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false",
|
"Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false",
|
||||||
ProviderConfigProperty.CLIENT_LIST_TYPE, null);
|
ProviderConfigProperty.CLIENT_LIST_TYPE, null);
|
||||||
configProperties.add(clientIdProperty);
|
configProperties.add(clientIdProperty);
|
||||||
|
@ -118,6 +129,27 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
||||||
return configProperties;
|
return configProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||||
|
Map<String, String> defaultValues = new HashMap<>();
|
||||||
|
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||||
|
|
||||||
|
defaultValues.put(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE, LDAPConstants.CN);
|
||||||
|
|
||||||
|
String roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||||
|
defaultValues.put(RoleMapperConfig.ROLE_OBJECT_CLASSES, roleObjectClasses);
|
||||||
|
|
||||||
|
defaultValues.put(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, LDAPConstants.MEMBER);
|
||||||
|
defaultValues.put(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, MembershipType.DN.toString());
|
||||||
|
|
||||||
|
String mode = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
|
||||||
|
defaultValues.put(RoleMapperConfig.MODE, mode);
|
||||||
|
|
||||||
|
defaultValues.put(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE);
|
||||||
|
defaultValues.put(RoleMapperConfig.USE_REALM_ROLES_MAPPING, "true");
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
|
@ -130,26 +162,30 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||||
checkMandatoryConfigAttribute(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN", mapperModel);
|
checkMandatoryConfigAttribute(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", mapperModel);
|
||||||
checkMandatoryConfigAttribute(RoleLDAPFederationMapper.MODE, "Mode", mapperModel);
|
checkMandatoryConfigAttribute(RoleMapperConfig.MODE, "Mode", mapperModel);
|
||||||
|
|
||||||
String realmMappings = mapperModel.getConfig().get(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING);
|
String realmMappings = mapperModel.getConfig().get(RoleMapperConfig.USE_REALM_ROLES_MAPPING);
|
||||||
boolean useRealmMappings = Boolean.parseBoolean(realmMappings);
|
boolean useRealmMappings = Boolean.parseBoolean(realmMappings);
|
||||||
if (!useRealmMappings) {
|
if (!useRealmMappings) {
|
||||||
String clientId = mapperModel.getConfig().get(RoleLDAPFederationMapper.CLIENT_ID);
|
String clientId = mapperModel.getConfig().get(RoleMapperConfig.CLIENT_ID);
|
||||||
if (clientId == null || clientId.trim().isEmpty()) {
|
if (clientId == null || clientId.trim().isEmpty()) {
|
||||||
throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used");
|
throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String customLdapFilter = mapperModel.getConfig().get(RoleLDAPFederationMapper.ROLES_LDAP_FILTER);
|
String customLdapFilter = mapperModel.getConfig().get(RoleMapperConfig.ROLES_LDAP_FILTER);
|
||||||
if ((customLdapFilter != null && customLdapFilter.trim().length() > 0) && (!customLdapFilter.startsWith("(") || !customLdapFilter.endsWith(")"))) {
|
if ((customLdapFilter != null && customLdapFilter.trim().length() > 0) && (!customLdapFilter.startsWith("(") || !customLdapFilter.endsWith(")"))) {
|
||||||
throw new MapperConfigValidationException("Custom Roles LDAP filter must starts with '(' and ends with ')'");
|
throw new MapperConfigValidationException("Custom Roles LDAP filter must starts with '(' and ends with ')'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserFederationMapper create(KeycloakSession session) {
|
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||||
return new RoleLDAPFederationMapper();
|
return new RoleLDAPFederationMapper(mapperModel, federationProvider, realm, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(String strategyKey) {
|
||||||
|
return userRolesStrategies.get(strategyKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package org.keycloak.federation.ldap.mappers.membership.role;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class RoleMapperConfig extends CommonLDAPGroupMapperConfig {
|
||||||
|
|
||||||
|
// LDAP DN where are roles of this tree saved.
|
||||||
|
public static final String ROLES_DN = "roles.dn";
|
||||||
|
|
||||||
|
// Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn"
|
||||||
|
public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute";
|
||||||
|
|
||||||
|
// Object classes of the role object.
|
||||||
|
public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
|
||||||
|
|
||||||
|
// Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID)
|
||||||
|
public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping";
|
||||||
|
|
||||||
|
// ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false
|
||||||
|
public static final String CLIENT_ID = "client.id";
|
||||||
|
|
||||||
|
// Customized LDAP filter which is added to the whole LDAP query
|
||||||
|
public static final String ROLES_LDAP_FILTER = "roles.ldap.filter";
|
||||||
|
|
||||||
|
// See UserRolesRetrieveStrategy
|
||||||
|
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE";
|
||||||
|
public static final String GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE";
|
||||||
|
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY";
|
||||||
|
|
||||||
|
|
||||||
|
public RoleMapperConfig(UserFederationMapperModel mapperModel) {
|
||||||
|
super(mapperModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRolesDn() {
|
||||||
|
String rolesDn = mapperModel.getConfig().get(ROLES_DN);
|
||||||
|
if (rolesDn == null) {
|
||||||
|
throw new ModelException("Roles DN is null! Check your configuration");
|
||||||
|
}
|
||||||
|
return rolesDn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLDAPGroupsDn() {
|
||||||
|
return getRolesDn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRoleNameLdapAttribute() {
|
||||||
|
String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE);
|
||||||
|
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLDAPGroupNameLdapAttribute() {
|
||||||
|
return getRoleNameLdapAttribute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<String> getRoleObjectClasses(LDAPFederationProvider ldapProvider) {
|
||||||
|
String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES);
|
||||||
|
if (objectClasses == null) {
|
||||||
|
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
|
||||||
|
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getConfigValues(objectClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomLdapFilter() {
|
||||||
|
return mapperModel.getConfig().get(ROLES_LDAP_FILTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRealmRolesMapping() {
|
||||||
|
String realmRolesMapping = mapperModel.getConfig().get(USE_REALM_ROLES_MAPPING);
|
||||||
|
return realmRolesMapping==null || Boolean.parseBoolean(realmRolesMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return mapperModel.getConfig().get(CLIENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getUserRolesRetrieveStrategy() {
|
||||||
|
String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY);
|
||||||
|
return strategyString!=null ? strategyString : LOAD_ROLES_BY_MEMBER_ATTRIBUTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory
|
org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory
|
||||||
org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory
|
org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory
|
||||||
org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory
|
org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory
|
||||||
|
org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory
|
|
@ -0,0 +1,108 @@
|
||||||
|
package org.keycloak.federation.ldap.idm.model;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupTreeResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class GroupTreeResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupResolvingCorrect() throws GroupTreeResolver.GroupTreeResolveException {
|
||||||
|
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3");
|
||||||
|
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group4", "group5");
|
||||||
|
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group6");
|
||||||
|
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4");
|
||||||
|
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5");
|
||||||
|
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7");
|
||||||
|
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
|
||||||
|
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7);
|
||||||
|
|
||||||
|
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||||
|
List<GroupTreeResolver.GroupTreeEntry> groupTree = resolver.resolveGroupTree(groups);
|
||||||
|
Assert.assertEquals(1, groupTree.size());
|
||||||
|
Assert.assertEquals("{ group1 -> [ { group2 -> [ { group4 -> [ ]}{ group5 -> [ ]} ]}{ group3 -> [ { group6 -> [ { group7 -> [ ]} ]} ]} ]}", groupTree.get(0).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupResolvingCorrect2_multipleRootGroups() throws GroupTreeResolver.GroupTreeResolveException {
|
||||||
|
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group8");
|
||||||
|
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
|
||||||
|
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2");
|
||||||
|
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5");
|
||||||
|
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group6", "group7");
|
||||||
|
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6");
|
||||||
|
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
|
||||||
|
GroupTreeResolver.Group group8 = new GroupTreeResolver.Group("group8", "group9");
|
||||||
|
GroupTreeResolver.Group group9 = new GroupTreeResolver.Group("group9");
|
||||||
|
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7, group8, group9);
|
||||||
|
|
||||||
|
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||||
|
List<GroupTreeResolver.GroupTreeEntry> groupTree = resolver.resolveGroupTree(groups);
|
||||||
|
|
||||||
|
Assert.assertEquals(2, groupTree.size());
|
||||||
|
Assert.assertEquals("{ group3 -> [ { group2 -> [ ]} ]}", groupTree.get(0).toString());
|
||||||
|
Assert.assertEquals("{ group4 -> [ { group1 -> [ { group8 -> [ { group9 -> [ ]} ]} ]}{ group5 -> [ { group6 -> [ ]}{ group7 -> [ ]} ]} ]}", groupTree.get(1).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupResolvingRecursion() {
|
||||||
|
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3");
|
||||||
|
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
|
||||||
|
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group4");
|
||||||
|
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group5");
|
||||||
|
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group1");
|
||||||
|
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7");
|
||||||
|
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
|
||||||
|
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7);
|
||||||
|
|
||||||
|
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||||
|
try {
|
||||||
|
resolver.resolveGroupTree(groups);
|
||||||
|
Assert.fail("Exception expected because of recursion");
|
||||||
|
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||||
|
Assert.assertTrue(gre.getMessage().startsWith("Recursion detected"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupResolvingMultipleParents() {
|
||||||
|
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2");
|
||||||
|
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
|
||||||
|
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2");
|
||||||
|
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5");
|
||||||
|
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group4");
|
||||||
|
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5);
|
||||||
|
|
||||||
|
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||||
|
try {
|
||||||
|
resolver.resolveGroupTree(groups);
|
||||||
|
Assert.fail("Exception expected because of some groups have multiple parents");
|
||||||
|
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||||
|
Assert.assertTrue(gre.getMessage().contains("detected to have multiple parents"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupResolvingMissingGroup() {
|
||||||
|
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2");
|
||||||
|
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group3");
|
||||||
|
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4");
|
||||||
|
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group4);
|
||||||
|
|
||||||
|
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||||
|
try {
|
||||||
|
resolver.resolveGroupTree(groups);
|
||||||
|
Assert.fail("Exception expected because of missing referenced group");
|
||||||
|
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||||
|
Assert.assertEquals("Group 'group3' referenced as member of group 'group2' doesn't exists", gre.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ public class LDAPDnTest {
|
||||||
|
|
||||||
dn.addFirst("uid", "Johny,Depp");
|
dn.addFirst("uid", "Johny,Depp");
|
||||||
Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString());
|
Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString());
|
||||||
|
Assert.assertEquals(LDAPDn.fromString("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org"), dn);
|
||||||
|
|
||||||
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
|
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
|
||||||
|
|
||||||
|
|
|
@ -1046,8 +1046,8 @@ module.controller('UserFederationMapperCtrl', function($scope, realm, provider,
|
||||||
function triggerMapperSync(direction) {
|
function triggerMapperSync(direction) {
|
||||||
UserFederationMapperSync.save({ direction: direction, realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, {}, function(syncResult) {
|
UserFederationMapperSync.save({ direction: direction, realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, {}, function(syncResult) {
|
||||||
Notifications.success("Data synced successfully. " + syncResult.status);
|
Notifications.success("Data synced successfully. " + syncResult.status);
|
||||||
}, function() {
|
}, function(error) {
|
||||||
Notifications.error("Error during sync of data");
|
Notifications.error(error.data.errorMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1066,13 +1066,7 @@ module.controller('UserFederationMapperCreateCtrl', function($scope, realm, prov
|
||||||
|
|
||||||
$scope.$watch('mapperType', function() {
|
$scope.$watch('mapperType', function() {
|
||||||
if ($scope.mapperType != null) {
|
if ($scope.mapperType != null) {
|
||||||
$scope.mapper.config = {};
|
$scope.mapper.config = $scope.mapperType.defaultConfig;
|
||||||
for ( var i = 0; i < $scope.mapperType.properties.length; i++) {
|
|
||||||
var property = $scope.mapperType.properties[i];
|
|
||||||
if (property.type === 'String' || property.type === 'boolean') {
|
|
||||||
$scope.mapper.config[ property.name ] = property.defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package org.keycloak.mappers;
|
package org.keycloak.mappers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
import org.keycloak.models.UserFederationProvider;
|
import org.keycloak.models.UserFederationProvider;
|
||||||
import org.keycloak.models.UserFederationSyncResult;
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,4 +40,9 @@ public interface UserFederationMapper extends Provider {
|
||||||
* @param realm
|
* @param realm
|
||||||
*/
|
*/
|
||||||
UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm);
|
UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return empty list if doesn't support storing of groups
|
||||||
|
*/
|
||||||
|
List<UserModel> getGroupMembers(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm, GroupModel group, int firstResult, int maxResults);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package org.keycloak.mappers;
|
package org.keycloak.mappers;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
import org.keycloak.provider.ConfiguredProvider;
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||||
|
@ -36,4 +39,11 @@ public interface UserFederationMapperFactory extends ProviderFactory<UserFederat
|
||||||
*/
|
*/
|
||||||
void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException;
|
void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to detect what are default values for ProviderConfigProperties specified during mapper creation
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@ import org.jboss.logging.Logger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -166,8 +168,40 @@ public class UserFederationManager implements UserProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
public List<UserModel> getGroupMembers(RealmModel realm, final GroupModel group, int firstResult, int maxResults) {
|
||||||
return session.userStorage().getGroupMembers(realm, group, firstResult, maxResults);
|
// Not very effective. For the page X, it is loading also all previous pages 0..X-1 . Improve if needed...
|
||||||
|
int maxTotal = firstResult + maxResults;
|
||||||
|
List<UserModel> localMembers = query(new PaginatedQuery() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserModel> query(RealmModel realm, int first, int max) {
|
||||||
|
return session.userStorage().getGroupMembers(realm, group, first, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, realm, 0, maxTotal);
|
||||||
|
|
||||||
|
Set<UserModel> result = new LinkedHashSet<>(localMembers);
|
||||||
|
|
||||||
|
for (UserFederationProviderModel federation : realm.getUserFederationProviders()) {
|
||||||
|
if (result.size() >= maxTotal) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int max = maxTotal - result.size();
|
||||||
|
|
||||||
|
UserFederationProvider fed = getFederationProvider(federation);
|
||||||
|
List<UserModel> current = fed.getGroupMembers(realm, group, 0, max);
|
||||||
|
if (current != null) {
|
||||||
|
result.addAll(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.size() <= firstResult) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
int max = Math.min(maxTotal, result.size());
|
||||||
|
return new ArrayList<>(result).subList(firstResult, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -104,6 +104,18 @@ public interface UserFederationProvider extends Provider {
|
||||||
*/
|
*/
|
||||||
List<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults);
|
List<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return group members from federation storage. Useful if info about group memberships is stored in the federation storage.
|
||||||
|
* Return empty list if your federation provider doesn't support storing user-group memberships
|
||||||
|
*
|
||||||
|
* @param realm
|
||||||
|
* @param group
|
||||||
|
* @param firstResult
|
||||||
|
* @param maxResults
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* called whenever a Realm is removed
|
* called whenever a Realm is removed
|
||||||
*
|
*
|
||||||
|
|
|
@ -188,6 +188,8 @@ public class UserFederationProviderResource {
|
||||||
propRep.setHelpText(prop.getHelpText());
|
propRep.setHelpText(prop.getHelpText());
|
||||||
rep.getProperties().add(propRep);
|
rep.getProperties().add(propRep);
|
||||||
}
|
}
|
||||||
|
rep.setDefaultConfig(mapperFactory.getDefaultConfig(this.federationProviderModel));
|
||||||
|
|
||||||
types.put(rep.getId(), rep);
|
types.put(rep.getId(), rep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,11 @@ public class DummyUserFederationProvider implements UserFederationProvider {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preRemove(RealmModel realm) {
|
public void preRemove(RealmModel realm) {
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package org.keycloak.testsuite.federation;
|
package org.keycloak.testsuite.federation.ldap;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
@ -11,10 +14,15 @@ import org.keycloak.federation.ldap.LDAPUtils;
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||||
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||||
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory;
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory;
|
||||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
|
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
|
||||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
|
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.role.RoleMapperConfig;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -22,6 +30,7 @@ import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserFederationMapperModel;
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
import org.keycloak.models.UserFederationProvider;
|
import org.keycloak.models.UserFederationProvider;
|
||||||
import org.keycloak.models.UserFederationProviderModel;
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserProvider;
|
import org.keycloak.models.UserProvider;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
@ -31,7 +40,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
class FederationTestUtils {
|
public class FederationTestUtils {
|
||||||
|
|
||||||
public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
|
public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
|
||||||
UserModel user = session.userStorage().addUser(realm, username);
|
UserModel user = session.userStorage().addUser(realm, username);
|
||||||
|
@ -75,7 +84,7 @@ class FederationTestUtils {
|
||||||
if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) {
|
if ("postal_code".equals(name) && postalCode != null && postalCode.length > 0) {
|
||||||
return Arrays.asList(postalCode);
|
return Arrays.asList(postalCode);
|
||||||
} else if ("street".equals(name) && street != null) {
|
} else if ("street".equals(name) && street != null) {
|
||||||
return Arrays.asList(street);
|
return Collections.singletonList(street);
|
||||||
} else {
|
} else {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
@ -98,6 +107,9 @@ class FederationTestUtils {
|
||||||
Assert.assertEquals(expectedPostalCode, user.getFirstAttribute("postal_code"));
|
Assert.assertEquals(expectedPostalCode, user.getFirstAttribute("postal_code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CRUD model mappers
|
||||||
|
|
||||||
public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) {
|
public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) {
|
||||||
addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE);
|
addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE);
|
||||||
}
|
}
|
||||||
|
@ -112,43 +124,72 @@ class FederationTestUtils {
|
||||||
return realm.addUserFederationMapper(mapperModel);
|
return realm.addUserFederationMapper(mapperModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addOrUpdateRoleLDAPMappers(RealmModel realm, UserFederationProviderModel providerModel, RoleLDAPFederationMapper.Mode mode) {
|
public static void addOrUpdateRoleLDAPMappers(RealmModel realm, UserFederationProviderModel providerModel, LDAPGroupMapperMode mode) {
|
||||||
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "realmRolesMapper");
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "realmRolesMapper");
|
||||||
if (mapperModel != null) {
|
if (mapperModel != null) {
|
||||||
mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString());
|
mapperModel.getConfig().put(RoleMapperConfig.MODE, mode.toString());
|
||||||
realm.updateUserFederationMapper(mapperModel);
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
} else {
|
} else {
|
||||||
String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN);
|
String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN);
|
||||||
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("realmRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID,
|
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("realmRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID,
|
||||||
RoleLDAPFederationMapper.ROLES_DN, "ou=RealmRoles," + baseDn,
|
RoleMapperConfig.ROLES_DN, "ou=RealmRoles," + baseDn,
|
||||||
RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "true",
|
RoleMapperConfig.USE_REALM_ROLES_MAPPING, "true",
|
||||||
RoleLDAPFederationMapper.MODE, mode.toString());
|
RoleMapperConfig.MODE, mode.toString());
|
||||||
realm.addUserFederationMapper(mapperModel);
|
realm.addUserFederationMapper(mapperModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "financeRolesMapper");
|
mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "financeRolesMapper");
|
||||||
if (mapperModel != null) {
|
if (mapperModel != null) {
|
||||||
mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString());
|
mapperModel.getConfig().put(RoleMapperConfig.MODE, mode.toString());
|
||||||
realm.updateUserFederationMapper(mapperModel);
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
} else {
|
} else {
|
||||||
String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN);
|
String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN);
|
||||||
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("financeRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID,
|
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("financeRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID,
|
||||||
RoleLDAPFederationMapper.ROLES_DN, "ou=FinanceRoles," + baseDn,
|
RoleMapperConfig.ROLES_DN, "ou=FinanceRoles," + baseDn,
|
||||||
RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "false",
|
RoleMapperConfig.USE_REALM_ROLES_MAPPING, "false",
|
||||||
RoleLDAPFederationMapper.CLIENT_ID, "finance",
|
RoleMapperConfig.CLIENT_ID, "finance",
|
||||||
RoleLDAPFederationMapper.MODE, mode.toString());
|
RoleMapperConfig.MODE, mode.toString());
|
||||||
realm.addUserFederationMapper(mapperModel);
|
realm.addUserFederationMapper(mapperModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void syncRolesFromLDAP(RealmModel realm, LDAPFederationProvider ldapProvider, UserFederationProviderModel providerModel) {
|
public static void addOrUpdateGroupMapper(RealmModel realm, UserFederationProviderModel providerModel, LDAPGroupMapperMode mode, String descriptionAttrName, String... otherConfigOptions) {
|
||||||
RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper();
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "groupsMapper");
|
||||||
|
if (mapperModel != null) {
|
||||||
|
mapperModel.getConfig().put(GroupMapperConfig.MODE, mode.toString());
|
||||||
|
updateGroupMapperConfigOptions(mapperModel, otherConfigOptions);
|
||||||
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
|
} else {
|
||||||
|
String baseDn = providerModel.getConfig().get(LDAPConstants.BASE_DN);
|
||||||
|
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("groupsMapper", providerModel.getId(), GroupLDAPFederationMapperFactory.PROVIDER_ID,
|
||||||
|
GroupMapperConfig.GROUPS_DN, "ou=Groups," + baseDn,
|
||||||
|
GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES, descriptionAttrName,
|
||||||
|
GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true",
|
||||||
|
GroupMapperConfig.MODE, mode.toString());
|
||||||
|
updateGroupMapperConfigOptions(mapperModel, otherConfigOptions);
|
||||||
|
realm.addUserFederationMapper(mapperModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updateGroupMapperConfigOptions(UserFederationMapperModel mapperModel, String... configOptions) {
|
||||||
|
for (int i=0 ; i<configOptions.length ; i+=2) {
|
||||||
|
String cfgName = configOptions[i];
|
||||||
|
String cfgValue = configOptions[i+1];
|
||||||
|
mapperModel.getConfig().put(cfgName, cfgValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End CRUD model mappers
|
||||||
|
|
||||||
|
public static void syncRolesFromLDAP(RealmModel realm, LDAPFederationProvider ldapProvider, UserFederationProviderModel providerModel) {
|
||||||
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "realmRolesMapper");
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "realmRolesMapper");
|
||||||
roleMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, ldapProvider.getSession(), realm);
|
RoleLDAPFederationMapper roleMapper = getRoleMapper(mapperModel, ldapProvider, realm);
|
||||||
|
|
||||||
|
roleMapper.syncDataFromFederationProviderToKeycloak();
|
||||||
|
|
||||||
mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "financeRolesMapper");
|
mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "financeRolesMapper");
|
||||||
roleMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, ldapProvider.getSession(), realm);
|
roleMapper = getRoleMapper(mapperModel, ldapProvider, realm);
|
||||||
|
roleMapper.syncDataFromFederationProviderToKeycloak();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) {
|
public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||||
|
@ -164,7 +205,17 @@ class FederationTestUtils {
|
||||||
public static void removeAllLDAPRoles(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) {
|
public static void removeAllLDAPRoles(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) {
|
||||||
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
|
||||||
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
LDAPQuery roleQuery = new RoleLDAPFederationMapper().createRoleQuery(mapperModel, ldapProvider);
|
LDAPQuery roleQuery = getRoleMapper(mapperModel, ldapProvider, appRealm).createRoleQuery();
|
||||||
|
List<LDAPObject> ldapRoles = roleQuery.getResultList();
|
||||||
|
for (LDAPObject ldapRole : ldapRoles) {
|
||||||
|
ldapProvider.getLdapIdentityStore().remove(ldapRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeAllLDAPGroups(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) {
|
||||||
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
LDAPQuery roleQuery = getGroupMapper(mapperModel, ldapProvider, appRealm).createGroupQuery();
|
||||||
List<LDAPObject> ldapRoles = roleQuery.getResultList();
|
List<LDAPObject> ldapRoles = roleQuery.getResultList();
|
||||||
for (LDAPObject ldapRole : ldapRoles) {
|
for (LDAPObject ldapRole : ldapRoles) {
|
||||||
ldapProvider.getLdapIdentityStore().remove(ldapRole);
|
ldapProvider.getLdapIdentityStore().remove(ldapRole);
|
||||||
|
@ -174,6 +225,36 @@ class FederationTestUtils {
|
||||||
public static void createLDAPRole(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName, String roleName) {
|
public static void createLDAPRole(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName, String roleName) {
|
||||||
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
|
||||||
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
new RoleLDAPFederationMapper().createLDAPRole(mapperModel, roleName, ldapProvider);
|
getRoleMapper(mapperModel, ldapProvider, appRealm).createLDAPRole(roleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LDAPObject createLDAPGroup(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String groupName, String... additionalAttrs) {
|
||||||
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
|
||||||
|
Map<String, Set<String>> additAttrs = new HashMap<>();
|
||||||
|
for (int i=0 ; i<additionalAttrs.length ; i+=2) {
|
||||||
|
String attrName = additionalAttrs[i];
|
||||||
|
String attrValue = additionalAttrs[i+1];
|
||||||
|
additAttrs.put(attrName, Collections.singleton(attrValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return getGroupMapper(mapperModel, ldapProvider, appRealm).createLDAPGroup(groupName, additAttrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GroupLDAPFederationMapper getGroupMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||||
|
return new GroupLDAPFederationMapper(mapperModel, ldapProvider, realm, new GroupLDAPFederationMapperFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RoleLDAPFederationMapper getRoleMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||||
|
return new RoleLDAPFederationMapper(mapperModel, ldapProvider, realm, new RoleLDAPFederationMapperFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void assertSyncEquals(UserFederationSyncResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved, int expectedFailed) {
|
||||||
|
Assert.assertEquals(expectedAdded, syncResult.getAdded());
|
||||||
|
Assert.assertEquals(expectedUpdated, syncResult.getUpdated());
|
||||||
|
Assert.assertEquals(expectedRemoved, syncResult.getRemoved());
|
||||||
|
Assert.assertEquals(expectedFailed, syncResult.getFailed());
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.keycloak.testsuite.federation;
|
package org.keycloak.testsuite.federation.ldap;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
|
@ -1,4 +1,4 @@
|
||||||
package org.keycloak.testsuite.federation;
|
package org.keycloak.testsuite.federation.ldap;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
|
@ -1,4 +1,4 @@
|
||||||
package org.keycloak.testsuite.federation;
|
package org.keycloak.testsuite.federation.ldap.base;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
|
@ -30,6 +30,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.OAuthClient;
|
import org.keycloak.testsuite.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
|
||||||
import org.keycloak.testsuite.pages.AccountPasswordPage;
|
import org.keycloak.testsuite.pages.AccountPasswordPage;
|
||||||
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
|
@ -0,0 +1,231 @@
|
||||||
|
package org.keycloak.testsuite.federation.ldap.base;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.services.managers.RealmManager;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
|
||||||
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
import org.keycloak.testsuite.rule.LDAPRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class LDAPGroupMapper2WaySyncTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static LDAPRule ldapRule = new LDAPRule();
|
||||||
|
|
||||||
|
private static UserFederationProviderModel ldapModel = null;
|
||||||
|
private static String descriptionAttrName = null;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
Map<String,String> ldapConfig = ldapRule.getConfig();
|
||||||
|
ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
|
||||||
|
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
|
||||||
|
|
||||||
|
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
|
||||||
|
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description";
|
||||||
|
|
||||||
|
// Add group mapper
|
||||||
|
FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName);
|
||||||
|
|
||||||
|
// Remove all LDAP groups
|
||||||
|
FederationTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper");
|
||||||
|
|
||||||
|
// Add some groups for testing into Keycloak
|
||||||
|
removeAllModelGroups(appRealm);
|
||||||
|
|
||||||
|
GroupModel group1 = appRealm.createGroup("group1");
|
||||||
|
appRealm.moveGroup(group1, null);
|
||||||
|
group1.setSingleAttribute(descriptionAttrName, "group1 - description1");
|
||||||
|
|
||||||
|
GroupModel group11 = appRealm.createGroup("group11");
|
||||||
|
appRealm.moveGroup(group11, group1);
|
||||||
|
|
||||||
|
GroupModel group12 = appRealm.createGroup("group12");
|
||||||
|
appRealm.moveGroup(group12, group1);
|
||||||
|
group12.setSingleAttribute(descriptionAttrName, "group12 - description12");
|
||||||
|
|
||||||
|
GroupModel group2 = appRealm.createGroup("group2");
|
||||||
|
appRealm.moveGroup(group2, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test01_syncNoPreserveGroupInheritance() throws Exception {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
|
||||||
|
// Update group mapper to skip preserve inheritance and check it will pass now
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false");
|
||||||
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
// Sync from Keycloak into LDAP
|
||||||
|
UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
// Delete all KC groups now
|
||||||
|
removeAllModelGroups(realm);
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11"));
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2"));
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
|
||||||
|
// Sync from LDAP back into Keycloak
|
||||||
|
UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
|
||||||
|
|
||||||
|
// Assert groups are imported to keycloak. All are at top level
|
||||||
|
GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
|
||||||
|
GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11");
|
||||||
|
GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12");
|
||||||
|
GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2");
|
||||||
|
|
||||||
|
Assert.assertEquals(0, kcGroup1.getSubGroups().size());
|
||||||
|
|
||||||
|
Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName));
|
||||||
|
|
||||||
|
// test drop non-existing works
|
||||||
|
testDropNonExisting(session, realm, mapperModel, ldapProvider);
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test02_syncWithGroupInheritance() throws Exception {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
|
||||||
|
// Update group mapper to skip preserve inheritance and check it will pass now
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true");
|
||||||
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
// Sync from Keycloak into LDAP
|
||||||
|
UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
// Delete all KC groups now
|
||||||
|
removeAllModelGroups(realm);
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11"));
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2"));
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
|
||||||
|
// Sync from LDAP back into Keycloak
|
||||||
|
UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
|
||||||
|
|
||||||
|
// Assert groups are imported to keycloak. All are at top level
|
||||||
|
GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
|
||||||
|
GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11");
|
||||||
|
GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12");
|
||||||
|
GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2");
|
||||||
|
|
||||||
|
Assert.assertEquals(2, kcGroup1.getSubGroups().size());
|
||||||
|
|
||||||
|
Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName));
|
||||||
|
|
||||||
|
// test drop non-existing works
|
||||||
|
testDropNonExisting(session, realm, mapperModel, ldapProvider);
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void removeAllModelGroups(RealmModel appRealm) {
|
||||||
|
for (GroupModel group : appRealm.getTopLevelGroups()) {
|
||||||
|
appRealm.removeGroup(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testDropNonExisting(KeycloakSession session, RealmModel realm, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
|
||||||
|
// Put some group directly to LDAP
|
||||||
|
FederationTestUtils.createLDAPGroup(session, realm, ldapModel, "group3");
|
||||||
|
|
||||||
|
// Sync and assert our group is still in LDAP
|
||||||
|
UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 0, 4, 0, 0);
|
||||||
|
Assert.assertNotNull(FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3"));
|
||||||
|
|
||||||
|
// Change config to drop non-existing groups
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true");
|
||||||
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
// Sync and assert group removed from LDAP
|
||||||
|
syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 0, 4, 1, 0);
|
||||||
|
Assert.assertNull(FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
package org.keycloak.testsuite.federation.ldap.base;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.FixMethodOrder;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.RuleChain;
|
||||||
|
import org.junit.rules.TestRule;
|
||||||
|
import org.junit.runners.MethodSorters;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
||||||
|
import org.keycloak.federation.ldap.LDAPUtils;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
|
import org.keycloak.models.UserFederationSyncResult;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.services.managers.RealmManager;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
|
||||||
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
import org.keycloak.testsuite.rule.LDAPRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
|
public class LDAPGroupMapperSyncTest {
|
||||||
|
|
||||||
|
private static LDAPRule ldapRule = new LDAPRule();
|
||||||
|
|
||||||
|
private static UserFederationProviderModel ldapModel = null;
|
||||||
|
private static String descriptionAttrName = null;
|
||||||
|
|
||||||
|
private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
Map<String,String> ldapConfig = ldapRule.getConfig();
|
||||||
|
ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
|
||||||
|
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
|
||||||
|
|
||||||
|
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
|
||||||
|
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description";
|
||||||
|
|
||||||
|
// Add group mapper
|
||||||
|
FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName);
|
||||||
|
|
||||||
|
// Add some groups for testing
|
||||||
|
LDAPObject group1 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description");
|
||||||
|
LDAPObject group11 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11");
|
||||||
|
LDAPObject group12 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description");
|
||||||
|
|
||||||
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false);
|
||||||
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static TestRule chain = RuleChain
|
||||||
|
.outerRule(ldapRule)
|
||||||
|
.around(keycloakRule);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test01_syncNoPreserveGroupInheritance() throws Exception {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm);
|
||||||
|
|
||||||
|
// Add recursive group mapping to LDAP. Check that sync with preserve group inheritance will fail
|
||||||
|
LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
|
||||||
|
LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12");
|
||||||
|
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
Assert.fail("Not expected group sync to pass");
|
||||||
|
} catch (ModelException expected) {
|
||||||
|
Assert.assertTrue(expected.getMessage().contains("Recursion detected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update group mapper to skip preserve inheritance and check it will pass now
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false");
|
||||||
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
|
||||||
|
// Assert groups are imported to keycloak. All are at top level
|
||||||
|
GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
|
||||||
|
GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11");
|
||||||
|
GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12");
|
||||||
|
|
||||||
|
Assert.assertEquals(0, kcGroup1.getSubGroups().size());
|
||||||
|
|
||||||
|
Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName));
|
||||||
|
|
||||||
|
// Cleanup - remove recursive mapping in LDAP
|
||||||
|
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test02_syncWithGroupInheritance() throws Exception {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm);
|
||||||
|
|
||||||
|
// Sync groups with inheritance
|
||||||
|
UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0);
|
||||||
|
|
||||||
|
// Assert groups are imported to keycloak including their inheritance from LDAP
|
||||||
|
GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11"));
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group12"));
|
||||||
|
GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11");
|
||||||
|
GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12");
|
||||||
|
|
||||||
|
Assert.assertEquals(2, kcGroup1.getSubGroups().size());
|
||||||
|
|
||||||
|
Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName));
|
||||||
|
|
||||||
|
// Update description attributes in LDAP
|
||||||
|
LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
|
||||||
|
group1.setSingleAttribute(descriptionAttrName, "group1 - changed description");
|
||||||
|
ldapProvider.getLdapIdentityStore().update(group1);
|
||||||
|
|
||||||
|
LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12");
|
||||||
|
group12.setAttribute(descriptionAttrName, null);
|
||||||
|
ldapProvider.getLdapIdentityStore().update(group12);
|
||||||
|
|
||||||
|
// Sync and assert groups updated
|
||||||
|
syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0);
|
||||||
|
|
||||||
|
// Assert attributes changed in keycloak
|
||||||
|
kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
|
||||||
|
kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12");
|
||||||
|
Assert.assertEquals("group1 - changed description", kcGroup1.getFirstAttribute(descriptionAttrName));
|
||||||
|
Assert.assertNull(kcGroup12.getFirstAttribute(descriptionAttrName));
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test03_syncWithDropNonExistingGroups() throws Exception {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
|
||||||
|
// Sync groups with inheritance
|
||||||
|
UserFederationSyncResult syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0);
|
||||||
|
|
||||||
|
// Assert groups are imported to keycloak including their inheritance from LDAP
|
||||||
|
GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"));
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"));
|
||||||
|
|
||||||
|
Assert.assertEquals(2, kcGroup1.getSubGroups().size());
|
||||||
|
|
||||||
|
// Create some new groups in keycloak
|
||||||
|
GroupModel model1 = realm.createGroup("model1");
|
||||||
|
realm.moveGroup(model1, null);
|
||||||
|
GroupModel model2 = realm.createGroup("model2");
|
||||||
|
kcGroup1.addChild(model2);
|
||||||
|
|
||||||
|
// Sync groups again from LDAP. Nothing deleted
|
||||||
|
syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
FederationTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0);
|
||||||
|
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"));
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"));
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/model1"));
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2"));
|
||||||
|
|
||||||
|
// Update group mapper to drop non-existing groups during sync
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true");
|
||||||
|
realm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
// Sync groups again from LDAP. Assert LDAP non-existing groups deleted
|
||||||
|
syncResult = new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||||
|
Assert.assertEquals(3, syncResult.getUpdated());
|
||||||
|
Assert.assertTrue(syncResult.getRemoved() >= 2);
|
||||||
|
|
||||||
|
// Sync and assert groups updated
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"));
|
||||||
|
Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"));
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/model1"));
|
||||||
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2"));
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
package org.keycloak.testsuite.federation.ldap.base;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.FixMethodOrder;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.RuleChain;
|
||||||
|
import org.junit.rules.TestRule;
|
||||||
|
import org.junit.runners.MethodSorters;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
|
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
||||||
|
import org.keycloak.federation.ldap.LDAPUtils;
|
||||||
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory;
|
||||||
|
import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig;
|
||||||
|
import org.keycloak.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserFederationProvider;
|
||||||
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.services.managers.RealmManager;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
|
||||||
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
import org.keycloak.testsuite.rule.LDAPRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
|
public class LDAPGroupMapperTest {
|
||||||
|
|
||||||
|
private static LDAPRule ldapRule = new LDAPRule();
|
||||||
|
|
||||||
|
private static UserFederationProviderModel ldapModel = null;
|
||||||
|
private static String descriptionAttrName = null;
|
||||||
|
|
||||||
|
private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app");
|
||||||
|
FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "john", "john@test.com", "password-app");
|
||||||
|
|
||||||
|
Map<String,String> ldapConfig = ldapRule.getConfig();
|
||||||
|
ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
|
||||||
|
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
|
||||||
|
|
||||||
|
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
|
||||||
|
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description";
|
||||||
|
|
||||||
|
// Add group mapper
|
||||||
|
FederationTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName);
|
||||||
|
|
||||||
|
// Add some groups for testing
|
||||||
|
LDAPObject group1 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description");
|
||||||
|
LDAPObject group11 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11");
|
||||||
|
LDAPObject group12 = FederationTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description");
|
||||||
|
|
||||||
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false);
|
||||||
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true);
|
||||||
|
|
||||||
|
// Sync LDAP groups to Keycloak DB
|
||||||
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
new GroupLDAPFederationMapperFactory().create(session).syncDataFromFederationProviderToKeycloak(mapperModel, ldapFedProvider, session, appRealm);
|
||||||
|
|
||||||
|
// Delete all LDAP users
|
||||||
|
FederationTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
|
||||||
|
|
||||||
|
// Add some LDAP users for testing
|
||||||
|
LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
|
||||||
|
ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1");
|
||||||
|
|
||||||
|
LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678");
|
||||||
|
ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1");
|
||||||
|
|
||||||
|
LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910");
|
||||||
|
ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1");
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static TestRule chain = RuleChain
|
||||||
|
.outerRule(ldapRule)
|
||||||
|
.around(keycloakRule);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test01_ldapOnlyGroupMappings() {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString());
|
||||||
|
appRealm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
|
||||||
|
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
|
||||||
|
|
||||||
|
// 1 - Grant some groups in LDAP
|
||||||
|
|
||||||
|
// This group should already exists as it was imported from LDAP
|
||||||
|
GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
|
||||||
|
john.joinGroup(group1);
|
||||||
|
|
||||||
|
// This group should already exists as it was imported from LDAP
|
||||||
|
GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
|
||||||
|
mary.joinGroup(group11);
|
||||||
|
|
||||||
|
// This group should already exists as it was imported from LDAP
|
||||||
|
GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
|
||||||
|
john.joinGroup(group12);
|
||||||
|
mary.joinGroup(group12);
|
||||||
|
|
||||||
|
// 2 - Check that group mappings are not in local Keycloak DB (They are in LDAP).
|
||||||
|
|
||||||
|
UserModel johnDb = session.userStorage().getUserByUsername("johnkeycloak", appRealm);
|
||||||
|
Set<GroupModel> johnDbGroups = johnDb.getGroups();
|
||||||
|
Assert.assertEquals(0, johnDbGroups.size());
|
||||||
|
|
||||||
|
// 3 - Check that group mappings are in LDAP and hence available through federation
|
||||||
|
|
||||||
|
Set<GroupModel> johnGroups = john.getGroups();
|
||||||
|
Assert.assertEquals(2, johnGroups.size());
|
||||||
|
Assert.assertTrue(johnGroups.contains(group1));
|
||||||
|
Assert.assertFalse(johnGroups.contains(group11));
|
||||||
|
Assert.assertTrue(johnGroups.contains(group12));
|
||||||
|
|
||||||
|
// 4 - Check through userProvider
|
||||||
|
List<UserModel> group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10);
|
||||||
|
List<UserModel> group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10);
|
||||||
|
List<UserModel> group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, group1Members.size());
|
||||||
|
Assert.assertEquals("johnkeycloak", group1Members.get(0).getUsername());
|
||||||
|
Assert.assertEquals(1, group11Members.size());
|
||||||
|
Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername());
|
||||||
|
Assert.assertEquals(2, group12Members.size());
|
||||||
|
|
||||||
|
// 4 - Delete some group mappings and check they are deleted
|
||||||
|
|
||||||
|
john.leaveGroup(group1);
|
||||||
|
john.leaveGroup(group12);
|
||||||
|
|
||||||
|
mary.leaveGroup(group1);
|
||||||
|
mary.leaveGroup(group12);
|
||||||
|
|
||||||
|
johnGroups = john.getGroups();
|
||||||
|
Assert.assertEquals(0, johnGroups.size());
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test02_readOnlyGroupMappings() {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.READ_ONLY.toString());
|
||||||
|
appRealm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
|
||||||
|
|
||||||
|
GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
|
||||||
|
GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
|
||||||
|
GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
|
||||||
|
|
||||||
|
// Add some group mappings directly into LDAP
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
|
||||||
|
|
||||||
|
LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
|
||||||
|
groupMapper.addGroupMappingInLDAP("group1", maryLdap);
|
||||||
|
groupMapper.addGroupMappingInLDAP("group11", maryLdap);
|
||||||
|
|
||||||
|
// Add some group mapping to model
|
||||||
|
mary.joinGroup(group12);
|
||||||
|
|
||||||
|
// Assert that mary has both LDAP and DB mapped groups
|
||||||
|
Set<GroupModel> maryGroups = mary.getGroups();
|
||||||
|
Assert.assertEquals(3, maryGroups.size());
|
||||||
|
Assert.assertTrue(maryGroups.contains(group1));
|
||||||
|
Assert.assertTrue(maryGroups.contains(group11));
|
||||||
|
Assert.assertTrue(maryGroups.contains(group12));
|
||||||
|
|
||||||
|
// Assert that access through DB will have just DB mapped groups
|
||||||
|
UserModel maryDB = session.userStorage().getUserByUsername("marykeycloak", appRealm);
|
||||||
|
Set<GroupModel> maryDBGroups = maryDB.getGroups();
|
||||||
|
Assert.assertFalse(maryDBGroups.contains(group1));
|
||||||
|
Assert.assertFalse(maryDBGroups.contains(group11));
|
||||||
|
Assert.assertTrue(maryDBGroups.contains(group12));
|
||||||
|
|
||||||
|
// Check through userProvider
|
||||||
|
List<UserModel> group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10);
|
||||||
|
List<UserModel> group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10);
|
||||||
|
List<UserModel> group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10);
|
||||||
|
Assert.assertEquals(1, group1Members.size());
|
||||||
|
Assert.assertEquals("marykeycloak", group1Members.get(0).getUsername());
|
||||||
|
Assert.assertEquals(1, group11Members.size());
|
||||||
|
Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername());
|
||||||
|
Assert.assertEquals(1, group12Members.size());
|
||||||
|
Assert.assertEquals("marykeycloak", group12Members.get(0).getUsername());
|
||||||
|
|
||||||
|
mary.leaveGroup(group12);
|
||||||
|
try {
|
||||||
|
mary.leaveGroup(group1);
|
||||||
|
Assert.fail("It wasn't expected to successfully delete LDAP group mappings in READ_ONLY mode");
|
||||||
|
} catch (ModelException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete role mappings directly in LDAP
|
||||||
|
deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group1");
|
||||||
|
deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group11");
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test03_importGroupMappings() {
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
try {
|
||||||
|
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
|
||||||
|
FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.IMPORT.toString());
|
||||||
|
appRealm.updateUserFederationMapper(mapperModel);
|
||||||
|
|
||||||
|
// Add some group mappings directly in LDAP
|
||||||
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
|
||||||
|
|
||||||
|
LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak");
|
||||||
|
groupMapper.addGroupMappingInLDAP("group11", robLdap);
|
||||||
|
groupMapper.addGroupMappingInLDAP("group12", robLdap);
|
||||||
|
|
||||||
|
// Get user and check that he has requested groupa from LDAP
|
||||||
|
UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm);
|
||||||
|
Set<GroupModel> robGroups = rob.getGroups();
|
||||||
|
|
||||||
|
GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
|
||||||
|
GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
|
||||||
|
GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
|
||||||
|
|
||||||
|
Assert.assertFalse(robGroups.contains(group1));
|
||||||
|
Assert.assertTrue(robGroups.contains(group11));
|
||||||
|
Assert.assertTrue(robGroups.contains(group12));
|
||||||
|
|
||||||
|
// Delete some group mappings in LDAP and check that it doesn't have any effect and user still has groups
|
||||||
|
deleteGroupMappingsInLDAP(groupMapper, robLdap, "group11");
|
||||||
|
deleteGroupMappingsInLDAP(groupMapper, robLdap, "group12");
|
||||||
|
robGroups = rob.getGroups();
|
||||||
|
Assert.assertTrue(robGroups.contains(group11));
|
||||||
|
Assert.assertTrue(robGroups.contains(group12));
|
||||||
|
|
||||||
|
// Delete group mappings through model and verifies that user doesn't have them anymore
|
||||||
|
rob.leaveGroup(group11);
|
||||||
|
rob.leaveGroup(group12);
|
||||||
|
robGroups = rob.getGroups();
|
||||||
|
Assert.assertEquals(0, robGroups.size());
|
||||||
|
} finally {
|
||||||
|
keycloakRule.stopSession(session, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteGroupMappingsInLDAP(GroupLDAPFederationMapper groupMapper, LDAPObject ldapUser, String groupName) {
|
||||||
|
LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName(groupName);
|
||||||
|
groupMapper.deleteGroupMappingInLDAP(ldapUser, ldapGroup);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.keycloak.testsuite.federation;
|
package org.keycloak.testsuite.federation.ldap.base;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -32,6 +32,8 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
|
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.OAuthClient;
|
import org.keycloak.testsuite.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.LDAPExampleServlet;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
import org.keycloak.testsuite.rule.LDAPRule;
|
import org.keycloak.testsuite.rule.LDAPRule;
|
||||||
|
@ -117,7 +119,6 @@ public class LDAPMultipleAttributesTest {
|
||||||
KeycloakSession session = keycloakRule.startSession();
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
try {
|
try {
|
||||||
RealmModel appRealm = session.realms().getRealmByName("test");
|
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||||
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
|
||||||
|
|
||||||
FederationTestUtils.assertUserImported(session.users(), appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", "88441");
|
FederationTestUtils.assertUserImported(session.users(), appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", "88441");
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.keycloak.testsuite.federation;
|
package org.keycloak.testsuite.federation.ldap.base;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -14,10 +14,8 @@ import org.junit.runners.MethodSorters;
|
||||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||||
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
||||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
import org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapper;
|
||||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
|
||||||
import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
|
|
||||||
import org.keycloak.models.AccountRoles;
|
import org.keycloak.models.AccountRoles;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
@ -32,6 +30,7 @@ import org.keycloak.models.UserFederationProviderModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.OAuthClient;
|
import org.keycloak.testsuite.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
@ -70,7 +69,7 @@ public class LDAPRoleMappingsTest {
|
||||||
ClientModel finance = appRealm.addClient("finance");
|
ClientModel finance = appRealm.addClient("finance");
|
||||||
|
|
||||||
// Delete all LDAP roles
|
// Delete all LDAP roles
|
||||||
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.LDAP_ONLY);
|
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY);
|
||||||
FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "realmRolesMapper");
|
FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "realmRolesMapper");
|
||||||
FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "financeRolesMapper");
|
FederationTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "financeRolesMapper");
|
||||||
|
|
||||||
|
@ -120,7 +119,7 @@ public class LDAPRoleMappingsTest {
|
||||||
try {
|
try {
|
||||||
RealmModel appRealm = session.realms().getRealmByName("test");
|
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.LDAP_ONLY);
|
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY);
|
||||||
|
|
||||||
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
|
UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
|
||||||
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
|
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
|
||||||
|
@ -212,7 +211,7 @@ public class LDAPRoleMappingsTest {
|
||||||
try {
|
try {
|
||||||
RealmModel appRealm = session.realms().getRealmByName("test");
|
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.READ_ONLY);
|
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.READ_ONLY);
|
||||||
|
|
||||||
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
|
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
|
||||||
|
|
||||||
|
@ -224,12 +223,13 @@ public class LDAPRoleMappingsTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add some role mappings directly into LDAP
|
// Add some role mappings directly into LDAP
|
||||||
RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper();
|
|
||||||
UserFederationMapperModel roleMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "realmRolesMapper");
|
UserFederationMapperModel roleMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "realmRolesMapper");
|
||||||
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
RoleLDAPFederationMapper roleMapper = FederationTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm);
|
||||||
|
|
||||||
LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
|
LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
|
||||||
roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, maryLdap);
|
roleMapper.addRoleMappingInLDAP("realmRole1", maryLdap);
|
||||||
roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, maryLdap);
|
roleMapper.addRoleMappingInLDAP("realmRole2", maryLdap);
|
||||||
|
|
||||||
// Add some role to model
|
// Add some role to model
|
||||||
mary.grantRole(realmRole3);
|
mary.grantRole(realmRole3);
|
||||||
|
@ -255,8 +255,8 @@ public class LDAPRoleMappingsTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete role mappings directly in LDAP
|
// Delete role mappings directly in LDAP
|
||||||
deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole1");
|
deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole1");
|
||||||
deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole2");
|
deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole2");
|
||||||
} finally {
|
} finally {
|
||||||
keycloakRule.stopSession(session, false);
|
keycloakRule.stopSession(session, false);
|
||||||
}
|
}
|
||||||
|
@ -282,15 +282,16 @@ public class LDAPRoleMappingsTest {
|
||||||
try {
|
try {
|
||||||
RealmModel appRealm = session.realms().getRealmByName("test");
|
RealmModel appRealm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, RoleLDAPFederationMapper.Mode.IMPORT);
|
FederationTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.IMPORT);
|
||||||
|
|
||||||
// Add some role mappings directly in LDAP
|
// Add some role mappings directly in LDAP
|
||||||
RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper();
|
|
||||||
UserFederationMapperModel roleMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "realmRolesMapper");
|
UserFederationMapperModel roleMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "realmRolesMapper");
|
||||||
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
|
||||||
|
RoleLDAPFederationMapper roleMapper = FederationTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm);
|
||||||
|
|
||||||
LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak");
|
LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak");
|
||||||
roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, robLdap);
|
roleMapper.addRoleMappingInLDAP("realmRole1", robLdap);
|
||||||
roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, robLdap);
|
roleMapper.addRoleMappingInLDAP("realmRole2", robLdap);
|
||||||
|
|
||||||
// Get user and check that he has requested roles from LDAP
|
// Get user and check that he has requested roles from LDAP
|
||||||
UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm);
|
UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm);
|
||||||
|
@ -311,8 +312,8 @@ public class LDAPRoleMappingsTest {
|
||||||
Assert.assertTrue(robRoles.contains(realmRole3));
|
Assert.assertTrue(robRoles.contains(realmRole3));
|
||||||
|
|
||||||
// Delete some role mappings in LDAP and check that it doesn't have any effect and user still has role
|
// Delete some role mappings in LDAP and check that it doesn't have any effect and user still has role
|
||||||
deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole1");
|
deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole1");
|
||||||
deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole2");
|
deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole2");
|
||||||
robRoles = rob.getRealmRoleMappings();
|
robRoles = rob.getRealmRoleMappings();
|
||||||
Assert.assertTrue(robRoles.contains(realmRole1));
|
Assert.assertTrue(robRoles.contains(realmRole1));
|
||||||
Assert.assertTrue(robRoles.contains(realmRole2));
|
Assert.assertTrue(robRoles.contains(realmRole2));
|
||||||
|
@ -330,12 +331,8 @@ public class LDAPRoleMappingsTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteRoleMappingsInLDAP(UserFederationMapperModel roleMapperModel, RoleLDAPFederationMapper roleMapper, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, String roleName) {
|
private void deleteRoleMappingsInLDAP(RoleLDAPFederationMapper roleMapper, LDAPObject ldapUser, String roleName) {
|
||||||
LDAPQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider);
|
LDAPObject ldapRole1 = roleMapper.loadLDAPRoleByName(roleName);
|
||||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
roleMapper.deleteRoleMappingInLDAP(ldapUser, ldapRole1);
|
||||||
Condition roleNameCondition = conditionsBuilder.equal(LDAPConstants.CN, roleName);
|
|
||||||
ldapQuery.addWhereCondition(roleNameCondition);
|
|
||||||
LDAPObject ldapRole1 = ldapQuery.getFirstResult();
|
|
||||||
roleMapper.deleteRoleMappingInLDAP(roleMapperModel, ldapProvider, ldapUser, ldapRole1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.keycloak.testsuite.federation;
|
package org.keycloak.testsuite.federation.ldap.base;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
|
@ -23,6 +23,7 @@ import org.keycloak.models.UserProvider;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.managers.UsersSyncManager;
|
import org.keycloak.services.managers.UsersSyncManager;
|
||||||
|
import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
import org.keycloak.testsuite.rule.LDAPRule;
|
import org.keycloak.testsuite.rule.LDAPRule;
|
||||||
import org.keycloak.testsuite.DummyUserFederationProviderFactory;
|
import org.keycloak.testsuite.DummyUserFederationProviderFactory;
|
||||||
|
@ -93,7 +94,7 @@ public class SyncProvidersTest {
|
||||||
try {
|
try {
|
||||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||||
UserFederationSyncResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, "test", ldapModel);
|
UserFederationSyncResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, "test", ldapModel);
|
||||||
assertSyncEquals(syncResult, 5, 0, 0, 0);
|
FederationTestUtils.assertSyncEquals(syncResult, 5, 0, 0, 0);
|
||||||
} finally {
|
} finally {
|
||||||
keycloakRule.stopSession(session, false);
|
keycloakRule.stopSession(session, false);
|
||||||
}
|
}
|
||||||
|
@ -139,7 +140,7 @@ public class SyncProvidersTest {
|
||||||
// Trigger partial sync
|
// Trigger partial sync
|
||||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||||
UserFederationSyncResult syncResult = usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel);
|
UserFederationSyncResult syncResult = usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel);
|
||||||
assertSyncEquals(syncResult, 1, 1, 0, 0);
|
FederationTestUtils.assertSyncEquals(syncResult, 1, 1, 0, 0);
|
||||||
} finally {
|
} finally {
|
||||||
keycloakRule.stopSession(session, false);
|
keycloakRule.stopSession(session, false);
|
||||||
}
|
}
|
||||||
|
@ -274,7 +275,7 @@ public class SyncProvidersTest {
|
||||||
FederationTestUtils.assertUserImported(session.users(), testRealm, "user1", "User1FN", "User1LN", "user1@email.org", "121");
|
FederationTestUtils.assertUserImported(session.users(), testRealm, "user1", "User1FN", "User1LN", "user1@email.org", "121");
|
||||||
FederationTestUtils.assertUserImported(session.users(), testRealm, "user2", "User2FN", "User2LN", "user2@email.org", "122");
|
FederationTestUtils.assertUserImported(session.users(), testRealm, "user2", "User2FN", "User2LN", "user2@email.org", "122");
|
||||||
UserModel user1 = session.users().getUserByUsername("user1", testRealm);
|
UserModel user1 = session.users().getUserByUsername("user1", testRealm);
|
||||||
Assert.assertEquals("user1", user1.getFirstAttribute(LDAPConstants.LDAP_ID));
|
Assert.assertEquals("user1", user1.getFirstAttribute(LDAPConstants.LDAP_ID));
|
||||||
|
|
||||||
// Revert config changes
|
// Revert config changes
|
||||||
UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderByDisplayName(ldapModel.getDisplayName(), testRealm);
|
UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderByDisplayName(ldapModel.getDisplayName(), testRealm);
|
||||||
|
@ -385,11 +386,4 @@ public class SyncProvidersTest {
|
||||||
throw new RuntimeException(ie);
|
throw new RuntimeException(ie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSyncEquals(UserFederationSyncResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved, int expectedFailed) {
|
|
||||||
Assert.assertEquals(expectedAdded, syncResult.getAdded());
|
|
||||||
Assert.assertEquals(expectedUpdated, syncResult.getUpdated());
|
|
||||||
Assert.assertEquals(expectedRemoved, syncResult.getRemoved());
|
|
||||||
Assert.assertEquals(expectedFailed, syncResult.getFailed());
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import java.net.URL;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.testsuite.federation.LDAPTestConfiguration;
|
import org.keycloak.testsuite.federation.ldap.LDAPTestConfiguration;
|
||||||
import org.keycloak.util.ldap.KerberosEmbeddedServer;
|
import org.keycloak.util.ldap.KerberosEmbeddedServer;
|
||||||
import org.keycloak.util.ldap.LDAPEmbeddedServer;
|
import org.keycloak.util.ldap.LDAPEmbeddedServer;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import org.junit.rules.ExternalResource;
|
import org.junit.rules.ExternalResource;
|
||||||
import org.keycloak.testsuite.federation.LDAPTestConfiguration;
|
import org.keycloak.testsuite.federation.ldap.LDAPTestConfiguration;
|
||||||
import org.keycloak.util.ldap.LDAPEmbeddedServer;
|
import org.keycloak.util.ldap.LDAPEmbeddedServer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,3 +18,8 @@ dn: ou=FinanceRoles,dc=keycloak,dc=org
|
||||||
objectclass: top
|
objectclass: top
|
||||||
objectclass: organizationalUnit
|
objectclass: organizationalUnit
|
||||||
ou: FinanceRoles
|
ou: FinanceRoles
|
||||||
|
|
||||||
|
dn: ou=Groups,dc=keycloak,dc=org
|
||||||
|
objectclass: top
|
||||||
|
objectclass: organizationalUnit
|
||||||
|
ou: Groups
|
||||||
|
|
Loading…
Reference in a new issue