KEYCLOAK-4640: LDAP memberships are being replaced instead of being added or deleted
This commit is contained in:
parent
996389d61b
commit
2602c222cd
13 changed files with 592 additions and 101 deletions
|
@ -38,6 +38,7 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import javax.naming.directory.SearchControls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow to directly call some operations against LDAPIdentityStore.
|
* Allow to directly call some operations against LDAPIdentityStore.
|
||||||
|
@ -167,30 +168,10 @@ public class LDAPUtils {
|
||||||
* @param memberChildAttrName used just if membershipType is UID. Usually 'uid'
|
* @param memberChildAttrName used just if membershipType is UID. Usually 'uid'
|
||||||
* @param ldapParent role or group
|
* @param ldapParent role or group
|
||||||
* @param ldapChild usually user (or child group or child role)
|
* @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(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
|
public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
|
||||||
|
|
||||||
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, memberChildAttrName);
|
String membership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
|
||||||
|
ldapProvider.getLdapIdentityStore().addMemberToGroup(ldapParent.getDn().toString(), memberAttrName, membership);
|
||||||
memberships.add(membership);
|
|
||||||
ldapParent.setAttribute(memberAttrName, memberships);
|
|
||||||
|
|
||||||
if (sendLDAPUpdateRequest) {
|
|
||||||
ldapProvider.getLdapIdentityStore().update(ldapParent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -204,29 +185,20 @@ public class LDAPUtils {
|
||||||
* @param ldapChild usually user (or child group or child role)
|
* @param ldapChild usually user (or child group or child role)
|
||||||
*/
|
*/
|
||||||
public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
|
public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
|
||||||
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
|
|
||||||
|
|
||||||
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
|
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
|
||||||
|
ldapProvider.getLdapIdentityStore().removeMemberFromGroup(ldapParent.getDn().toString(), memberAttrName, userMembership);
|
||||||
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
|
* Return all existing memberships (values of attribute 'member' ) from the given ldapRole or ldapGroup
|
||||||
*
|
*
|
||||||
|
* @param ldapProvider The ldap provider
|
||||||
* @param memberAttrName usually 'member'
|
* @param memberAttrName usually 'member'
|
||||||
* @param ldapRole
|
* @param ldapRole
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static Set<String> getExistingMemberships(String memberAttrName, LDAPObject ldapRole) {
|
public static Set<String> getExistingMemberships(LDAPStorageProvider ldapProvider, String memberAttrName, LDAPObject ldapRole) {
|
||||||
|
LDAPUtils.fillRangedAttribute(ldapProvider, ldapRole, memberAttrName);
|
||||||
Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
|
Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
|
||||||
if (memberships == null) {
|
if (memberships == null) {
|
||||||
memberships = new HashSet<>();
|
memberships = new HashSet<>();
|
||||||
|
@ -298,4 +270,27 @@ public class LDAPUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LDAPQuery createLdapQueryForRangeAttribute(LDAPStorageProvider ldapProvider, LDAPObject ldapObject, String name) {
|
||||||
|
LDAPQuery q = new LDAPQuery(ldapProvider);
|
||||||
|
q.setSearchDn(ldapObject.getDn().toString());
|
||||||
|
q.setSearchScope(SearchControls.OBJECT_SCOPE);
|
||||||
|
q.addReturningLdapAttribute(name + ";range=" + (ldapObject.getCurrentRange(name) + 1) + "-*");
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs iterative searches over an LDAPObject to return an attribute that is ranged.
|
||||||
|
* @param ldapProvider The provider to use
|
||||||
|
* @param ldapObject The current object with the ranged attribute not complete
|
||||||
|
* @param name The attribute name
|
||||||
|
*/
|
||||||
|
public static void fillRangedAttribute(LDAPStorageProvider ldapProvider, LDAPObject ldapObject, String name) {
|
||||||
|
LDAPObject newObject = ldapObject;
|
||||||
|
while (!newObject.isRangeComplete(name)) {
|
||||||
|
LDAPQuery q = createLdapQueryForRangeAttribute(ldapProvider, ldapObject, name);
|
||||||
|
newObject = q.getFirstResult();
|
||||||
|
ldapObject.populateRangedAttribute(newObject, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,8 @@ public class LDAPObject {
|
||||||
// Copy of "attributes" containing lower-cased keys
|
// Copy of "attributes" containing lower-cased keys
|
||||||
private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
|
private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
|
||||||
|
|
||||||
|
// range attributes are always read from 0 to max so just saving the top value
|
||||||
|
private final Map<String, Integer> rangedAttributes = new HashMap<>();
|
||||||
|
|
||||||
public String getUuid() {
|
public String getUuid() {
|
||||||
return uuid;
|
return uuid;
|
||||||
|
@ -123,6 +125,36 @@ public class LDAPObject {
|
||||||
return (values == null) ? null : new LinkedHashSet<>(values);
|
return (values == null) ? null : new LinkedHashSet<>(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRangeComplete(String name) {
|
||||||
|
return !rangedAttributes.containsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentRange(String name) {
|
||||||
|
return rangedAttributes.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRangeCompleteForAllAttributes() {
|
||||||
|
return rangedAttributes.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRangedAttribute(String name, int max) {
|
||||||
|
Integer current = rangedAttributes.get(name);
|
||||||
|
if (current == null || max > current) {
|
||||||
|
rangedAttributes.put(name, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateRangedAttribute(LDAPObject obj, String name) {
|
||||||
|
Set<String> newValues = obj.getAttributes().get(name);
|
||||||
|
if (newValues != null && attributes.containsKey(name)) {
|
||||||
|
attributes.get(name).addAll(newValues);
|
||||||
|
if (!obj.isRangeComplete(name)) {
|
||||||
|
addRangedAttribute(name, obj.getCurrentRange(name));
|
||||||
|
} else {
|
||||||
|
rangedAttributes.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Set<String>> getAttributes() {
|
public Map<String, Set<String>> getAttributes() {
|
||||||
return attributes;
|
return attributes;
|
||||||
|
@ -152,6 +184,7 @@ public class LDAPObject {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "LDAP Object [ dn: " + dn + " , uuid: " + uuid + ", attributes: " + attributes + ", readOnly attribute names: " + readOnlyAttributeNames + " ]";
|
return "LDAP Object [ dn: " + dn + " , uuid: " + uuid + ", attributes: " + attributes +
|
||||||
|
", readOnly attribute names: " + readOnlyAttributeNames + ", ranges: " + rangedAttributes + " ]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,22 @@ public interface IdentityStore {
|
||||||
*/
|
*/
|
||||||
void remove(LDAPObject ldapObject);
|
void remove(LDAPObject ldapObject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a member to a group.
|
||||||
|
* @param groupDn The DN of the group object
|
||||||
|
* @param memberAttrName The member attribute name
|
||||||
|
* @param value The value (it can be uid or dn depending the group type)
|
||||||
|
*/
|
||||||
|
public void addMemberToGroup(String groupDn, String memberAttrName, String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a member from a group.
|
||||||
|
* @param groupDn The DN of the group object
|
||||||
|
* @param memberAttrName The member attribute name
|
||||||
|
* @param value The value (it can be uid or dn depending the group type)
|
||||||
|
*/
|
||||||
|
public void removeMemberFromGroup(String groupDn, String memberAttrName, String value);
|
||||||
|
|
||||||
// Identity query
|
// Identity query
|
||||||
|
|
||||||
List<LDAPObject> fetchQueryResults(LDAPQuery LDAPQuery);
|
List<LDAPObject> fetchQueryResults(LDAPQuery LDAPQuery);
|
||||||
|
|
|
@ -54,6 +54,11 @@ import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.naming.directory.AttributeInUseException;
|
||||||
|
import javax.naming.directory.NoSuchAttributeException;
|
||||||
|
import javax.naming.directory.SchemaViolationException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IdentityStore implementation backed by an LDAP directory
|
* An IdentityStore implementation backed by an LDAP directory
|
||||||
|
@ -65,6 +70,7 @@ import java.util.TreeSet;
|
||||||
public class LDAPIdentityStore implements IdentityStore {
|
public class LDAPIdentityStore implements IdentityStore {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(LDAPIdentityStore.class);
|
private static final Logger logger = Logger.getLogger(LDAPIdentityStore.class);
|
||||||
|
private static final Pattern rangePattern = Pattern.compile("([^;]+);range=([0-9]+)-([0-9]+|\\*)");
|
||||||
|
|
||||||
private final LDAPConfig config;
|
private final LDAPConfig config;
|
||||||
private final LDAPOperationManager operationManager;
|
private final LDAPOperationManager operationManager;
|
||||||
|
@ -101,6 +107,44 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMemberToGroup(String groupDn, String memberAttrName, String value) {
|
||||||
|
// do not check EMPTY_MEMBER_ATTRIBUTE_VALUE, we save one useless query
|
||||||
|
// the value will be there forever for objectclasses that enforces the attribute as MUST
|
||||||
|
BasicAttribute attr = new BasicAttribute(memberAttrName, value);
|
||||||
|
ModificationItem item = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr);
|
||||||
|
try {
|
||||||
|
this.operationManager.modifyAttributesNaming(groupDn, new ModificationItem[]{item}, null);
|
||||||
|
} catch (AttributeInUseException e) {
|
||||||
|
logger.debugf("Group %s already contains the member %s", groupDn, value);
|
||||||
|
} catch (NamingException e) {
|
||||||
|
throw new ModelException("Could not modify attribute for DN [" + groupDn + "]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeMemberFromGroup(String groupDn, String memberAttrName, String value) {
|
||||||
|
BasicAttribute attr = new BasicAttribute(memberAttrName, value);
|
||||||
|
ModificationItem item = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attr);
|
||||||
|
try {
|
||||||
|
this.operationManager.modifyAttributesNaming(groupDn, new ModificationItem[]{item}, null);
|
||||||
|
} catch (NoSuchAttributeException e) {
|
||||||
|
logger.debugf("Group %s does not contain the member %s", groupDn, value);
|
||||||
|
} catch (SchemaViolationException e) {
|
||||||
|
// schema violation removing one member => add the empty attribute, it cannot be other thing
|
||||||
|
logger.infof("Schema violation in group %s removing member %s. Trying adding empty member attribute.", groupDn, value);
|
||||||
|
try {
|
||||||
|
this.operationManager.modifyAttributesNaming(groupDn,
|
||||||
|
new ModificationItem[]{item, new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute(memberAttrName, LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE))},
|
||||||
|
null);
|
||||||
|
} catch (NamingException ex) {
|
||||||
|
throw new ModelException("Could not modify attribute for DN [" + groupDn + "]", ex);
|
||||||
|
}
|
||||||
|
} catch (NamingException e) {
|
||||||
|
throw new ModelException("Could not modify attribute for DN [" + groupDn + "]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void update(LDAPObject ldapObject) {
|
public void update(LDAPObject ldapObject) {
|
||||||
checkRename(ldapObject);
|
checkRename(ldapObject);
|
||||||
|
@ -199,7 +243,8 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (SearchResult result : search) {
|
for (SearchResult result : search) {
|
||||||
if (!result.getNameInNamespace().equalsIgnoreCase(baseDN)) {
|
// don't add the branch in subtree search
|
||||||
|
if (identityQuery.getSearchScope() != SearchControls.SUBTREE_SCOPE || !result.getNameInNamespace().equalsIgnoreCase(baseDN)) {
|
||||||
results.add(populateAttributedType(result, identityQuery));
|
results.add(populateAttributedType(result, identityQuery));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -351,6 +396,21 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
|
|
||||||
String ldapAttributeName = ldapAttribute.getID();
|
String ldapAttributeName = ldapAttribute.getID();
|
||||||
|
|
||||||
|
// check for ranged attribute
|
||||||
|
Matcher m = rangePattern.matcher(ldapAttributeName);
|
||||||
|
if (m.matches()) {
|
||||||
|
ldapAttributeName = m.group(1);
|
||||||
|
// range=X-* means all the attributes returned
|
||||||
|
if (!m.group(3).equals("*")) {
|
||||||
|
try {
|
||||||
|
int max = Integer.parseInt(m.group(3));
|
||||||
|
ldapObject.addRangedAttribute(ldapAttributeName, max);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.warnf("Invalid ranged expresion for attribute: %s", m.group(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName())) {
|
if (ldapAttributeName.equalsIgnoreCase(getConfig().getUuidLDAPAttributeName())) {
|
||||||
Object uuidValue = ldapAttribute.get();
|
Object uuidValue = ldapAttribute.get();
|
||||||
ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
|
ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
|
||||||
|
|
|
@ -528,8 +528,7 @@ public class LDAPOperationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void modifyAttributes(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) {
|
public void modifyAttributesNaming(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) throws NamingException {
|
||||||
try {
|
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.tracef("Modifying attributes for entry [%s]: [", dn);
|
logger.tracef("Modifying attributes for entry [%s]: [", dn);
|
||||||
|
|
||||||
|
@ -561,7 +560,6 @@ public class LDAPOperationManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new StringBuilder("LdapOperation: modify\n")
|
return new StringBuilder("LdapOperation: modify\n")
|
||||||
|
@ -570,8 +568,12 @@ public class LDAPOperationManager {
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}, null, decorator);
|
}, null, decorator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void modifyAttributes(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) {
|
||||||
|
try {
|
||||||
|
modifyAttributesNaming(dn, mods, decorator);
|
||||||
} catch (NamingException e) {
|
} catch (NamingException e) {
|
||||||
throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
|
throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,12 +50,12 @@ public enum MembershipType {
|
||||||
@Override
|
@Override
|
||||||
public Set<LDAPDn> getLDAPSubgroups(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup) {
|
public Set<LDAPDn> getLDAPSubgroups(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup) {
|
||||||
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
|
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
|
||||||
return getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), LDAPDn.fromString(config.getLDAPGroupsDn()));
|
return getLDAPMembersWithParent(groupMapper.getLdapProvider(), ldapGroup, config.getMembershipLdapAttribute(), LDAPDn.fromString(config.getLDAPGroupsDn()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get just those members of specified group, which are descendants of "requiredParentDn"
|
// Get just those members of specified group, which are descendants of "requiredParentDn"
|
||||||
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
|
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPStorageProvider ldapProvider, LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
|
||||||
Set<String> allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup);
|
Set<String> allMemberships = LDAPUtils.getExistingMemberships(ldapProvider, membershipLdapAttribute, ldapGroup);
|
||||||
|
|
||||||
// Filter and keep just descendants of requiredParentDn
|
// Filter and keep just descendants of requiredParentDn
|
||||||
Set<LDAPDn> result = new HashSet<>();
|
Set<LDAPDn> result = new HashSet<>();
|
||||||
|
@ -74,7 +74,7 @@ public enum MembershipType {
|
||||||
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
|
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
|
||||||
|
|
||||||
LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
||||||
Set<LDAPDn> userDns = getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), usersDn);
|
Set<LDAPDn> userDns = getLDAPMembersWithParent(ldapProvider, ldapGroup, config.getMembershipLdapAttribute(), usersDn);
|
||||||
|
|
||||||
if (userDns == null) {
|
if (userDns == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
@ -139,7 +139,7 @@ public enum MembershipType {
|
||||||
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
|
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
|
||||||
|
|
||||||
String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute();
|
String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute();
|
||||||
Set<String> memberUids = LDAPUtils.getExistingMemberships(memberAttrName, ldapGroup);
|
Set<String> memberUids = LDAPUtils.getExistingMemberships(ldapProvider, memberAttrName, ldapGroup);
|
||||||
|
|
||||||
if (memberUids == null || memberUids.size() <= firstResult) {
|
if (memberUids == null || memberUids.size() <= firstResult) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
|
@ -465,8 +465,10 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
||||||
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
|
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
|
||||||
for (GroupModel kcSubgroup : kcSubgroups) {
|
for (GroupModel kcSubgroup : kcSubgroups) {
|
||||||
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
|
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
|
||||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapSubgroup, false);
|
if (!toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn())) {
|
||||||
toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn());
|
// if the group is not in the ldap group => add it
|
||||||
|
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapSubgroup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove LDAP subgroups, which are not members in KC anymore
|
// Remove LDAP subgroups, which are not members in KC anymore
|
||||||
|
@ -476,11 +478,6 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
||||||
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, fakeGroup);
|
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, fakeGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update group to LDAP
|
|
||||||
if (!kcGroup.getSubGroups().isEmpty() || !toRemoveSubgroupsDNs.isEmpty()) {
|
|
||||||
ldapProvider.getLdapIdentityStore().update(ldapGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
|
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
|
||||||
processKeycloakGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
|
processKeycloakGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
|
||||||
}
|
}
|
||||||
|
@ -510,6 +507,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel kcGroup, int firstResult, int maxResults) {
|
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel kcGroup, int firstResult, int maxResults) {
|
||||||
|
// TODO: with ranged search in AD we can improve the search using the specific range (not done for the moment)
|
||||||
LDAPObject ldapGroup = loadLDAPGroupByName(kcGroup.getName());
|
LDAPObject ldapGroup = loadLDAPGroupByName(kcGroup.getName());
|
||||||
if (ldapGroup == null) {
|
if (ldapGroup == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
@ -539,7 +537,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
||||||
// Finally update LDAP membership in the parent group
|
// Finally update LDAP membership in the parent group
|
||||||
if (highestGroupToSync.getParent() != null) {
|
if (highestGroupToSync.getParent() != null) {
|
||||||
LDAPObject ldapParentGroup = loadLDAPGroupByName(highestGroupToSync.getParent().getName());
|
LDAPObject ldapParentGroup = loadLDAPGroupByName(highestGroupToSync.getParent().getName());
|
||||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), getMembershipUserLdapAttribute(), ldapParentGroup, ldapGroup, true);
|
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), getMembershipUserLdapAttribute(), ldapParentGroup, ldapGroup);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No care about group inheritance. Let's just sync current group
|
// No care about group inheritance. Let's just sync current group
|
||||||
|
@ -551,7 +549,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
||||||
|
|
||||||
String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
|
String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
|
||||||
|
|
||||||
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapUser, true);
|
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) {
|
public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) {
|
||||||
|
|
|
@ -261,7 +261,7 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
||||||
|
|
||||||
String membershipUserAttrName = getMembershipUserLdapAttribute();
|
String membershipUserAttrName = getMembershipUserLdapAttribute();
|
||||||
|
|
||||||
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserAttrName, ldapRole, ldapUser, true);
|
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserAttrName, ldapRole, ldapUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) {
|
public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) {
|
||||||
|
|
|
@ -142,17 +142,17 @@ public class TestLDAPResource {
|
||||||
LDAPObject defaultGroup15 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup15", descriptionAttrName, "Default Group15 - description");
|
LDAPObject defaultGroup15 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "defaultGroup15", descriptionAttrName, "Default Group15 - description");
|
||||||
LDAPObject teamSubChild20262027 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team SubChild 2026/2027", descriptionAttrName, "A sub child group with slashes in the name");
|
LDAPObject teamSubChild20262027 = LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "Team SubChild 2026/2027", descriptionAttrName, "A sub child group with slashes in the name");
|
||||||
|
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11, false);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12);
|
||||||
|
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup11, false);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup11);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup12, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup12);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, teamChild20182019, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, teamChild20182019);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamChild20182019, teamSubChild20202021, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamChild20182019, teamSubChild20202021);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup13, teamSubChild20222023, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup13, teamSubChild20222023);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamSubChild20222023, defaultGroup14, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamSubChild20222023, defaultGroup14);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamRoot20242025, defaultGroup15, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamRoot20242025, defaultGroup15);
|
||||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup15, teamSubChild20262027, true);
|
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup15, teamSubChild20262027);
|
||||||
|
|
||||||
// Sync LDAP groups to Keycloak DB
|
// Sync LDAP groups to Keycloak DB
|
||||||
ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(realm, ldapModel, "groupsMapper");
|
ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(realm, ldapModel, "groupsMapper");
|
||||||
|
|
|
@ -99,8 +99,8 @@ public class LDAPGroupMapperSyncTest extends AbstractLDAPTest {
|
||||||
LDAPObject group11 = LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "group11");
|
LDAPObject group11 = LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "group11");
|
||||||
LDAPObject group12 = LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "group12", descriptionAttrName, "group12 - description");
|
LDAPObject group12 = LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "group12", descriptionAttrName, "group12 - description");
|
||||||
|
|
||||||
LDAPUtils.addMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11, false);
|
LDAPUtils.addMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11);
|
||||||
LDAPUtils.addMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12, true);
|
LDAPUtils.addMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ public class LDAPGroupMapperSyncTest extends AbstractLDAPTest {
|
||||||
// Add recursive group mapping to LDAP. Check that sync with preserve group inheritance will fail
|
// Add recursive group mapping to LDAP. Check that sync with preserve group inheritance will fail
|
||||||
LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
|
LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
|
||||||
LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12");
|
LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12");
|
||||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group12, group1, true);
|
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group12, group1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(realm);
|
new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(realm);
|
||||||
|
@ -298,7 +298,7 @@ public class LDAPGroupMapperSyncTest extends AbstractLDAPTest {
|
||||||
GroupMapperConfig groupMapperConfig = new GroupMapperConfig(mapperModel);
|
GroupMapperConfig groupMapperConfig = new GroupMapperConfig(mapperModel);
|
||||||
LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName("group11");
|
LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName("group11");
|
||||||
LDAPUtils.addMember(ldapProvider, groupMapperConfig.getMembershipTypeLdapAttribute(), groupMapperConfig.getMembershipLdapAttribute(),
|
LDAPUtils.addMember(ldapProvider, groupMapperConfig.getMembershipTypeLdapAttribute(), groupMapperConfig.getMembershipLdapAttribute(),
|
||||||
groupMapperConfig.getMembershipUserLdapAttribute(ldapProvider.getLdapIdentityStore().getConfig()), ldapGroup, johnLdap, true);
|
groupMapperConfig.getMembershipUserLdapAttribute(ldapProvider.getLdapIdentityStore().getConfig()), ldapGroup, johnLdap);
|
||||||
|
|
||||||
// Assert groups not yet imported to Keycloak DB
|
// Assert groups not yet imported to Keycloak DB
|
||||||
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
|
Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
|
||||||
|
|
|
@ -19,6 +19,8 @@ package org.keycloak.testsuite.federation.ldap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.naming.directory.SearchControls;
|
||||||
|
|
||||||
import org.jboss.arquillian.container.test.api.Deployment;
|
import org.jboss.arquillian.container.test.api.Deployment;
|
||||||
import org.jboss.arquillian.container.test.api.TargetsContainer;
|
import org.jboss.arquillian.container.test.api.TargetsContainer;
|
||||||
|
@ -45,6 +47,7 @@ import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
||||||
import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig;
|
import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig;
|
||||||
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
||||||
import org.keycloak.testsuite.util.LDAPRule;
|
import org.keycloak.testsuite.util.LDAPRule;
|
||||||
|
@ -476,14 +479,14 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
|
||||||
|
|
||||||
// 2 - Add one existing user rob to LDAP group
|
// 2 - Add one existing user rob to LDAP group
|
||||||
LDAPObject jamesLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "jameskeycloak");
|
LDAPObject jamesLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "jameskeycloak");
|
||||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, jamesLdap, false);
|
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, jamesLdap);
|
||||||
|
|
||||||
// 3 - Add non-existing user to LDAP group
|
// 3 - Add non-existing user to LDAP group
|
||||||
LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
||||||
nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent");
|
nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent");
|
||||||
LDAPObject nonExistentLdapUser = new LDAPObject();
|
LDAPObject nonExistentLdapUser = new LDAPObject();
|
||||||
nonExistentLdapUser.setDn(nonExistentDn);
|
nonExistentLdapUser.setDn(nonExistentDn);
|
||||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, nonExistentLdapUser, true);
|
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, nonExistentLdapUser);
|
||||||
|
|
||||||
// 4 - Check group members. Just existing user rob should be present
|
// 4 - Check group members. Just existing user rob should be present
|
||||||
groupMapper.syncDataFromFederationProviderToKeycloak(appRealm);
|
groupMapper.syncDataFromFederationProviderToKeycloak(appRealm);
|
||||||
|
@ -678,4 +681,111 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LDAPObject searchObjectInBase(LDAPStorageProvider ldapProvider, String dn, String... attrs) {
|
||||||
|
LDAPQuery q = new LDAPQuery(ldapProvider)
|
||||||
|
.setSearchDn(dn)
|
||||||
|
.setSearchScope(SearchControls.OBJECT_SCOPE);
|
||||||
|
if (attrs != null) {
|
||||||
|
for (String attr: attrs) {
|
||||||
|
q.addReturningLdapAttribute(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return q.getFirstResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test08_ldapOnlyGroupMappingsRanged() {
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
int membersToTest = 61; // try to do 3 pages (30+30+1)
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel appRealm = ctx.getRealm();
|
||||||
|
|
||||||
|
ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(appRealm, ctx.getLdapModel(), "groupsMapper");
|
||||||
|
LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString());
|
||||||
|
appRealm.updateComponent(mapperModel);
|
||||||
|
|
||||||
|
// create big grups that use ranged search
|
||||||
|
String descriptionAttrName = getGroupDescriptionLDAPAttrName(ctx.getLdapProvider());
|
||||||
|
LDAPObject bigGroup = LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "biggroup", descriptionAttrName, "biggroup - description");
|
||||||
|
// create the users to use range search and add them to the group
|
||||||
|
for (int i = 0; i < membersToTest; i++) {
|
||||||
|
String username = String.format("user%02d", i);
|
||||||
|
LDAPObject user = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, username, username, username, username + "@email.org", null, "1234");
|
||||||
|
LDAPUtils.addMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, "not-used", bigGroup, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if ranged intercetor is in place and working
|
||||||
|
GroupMapperConfig config = new GroupMapperConfig(mapperModel);
|
||||||
|
bigGroup = LDAPGroupMapperTest.searchObjectInBase(ctx.getLdapProvider(), bigGroup.getDn().toString(), config.getMembershipLdapAttribute());
|
||||||
|
Assert.assertNotNull(bigGroup.getAttributes().get(config.getMembershipLdapAttribute()));
|
||||||
|
Assert.assertFalse(bigGroup.isRangeComplete(config.getMembershipLdapAttribute()));
|
||||||
|
Assert.assertTrue(membersToTest > bigGroup.getAttributeAsSet(config.getMembershipLdapAttribute()).size());
|
||||||
|
Assert.assertEquals(bigGroup.getCurrentRange(config.getMembershipLdapAttribute()), bigGroup.getAttributeAsSet(config.getMembershipLdapAttribute()).size() - 1);
|
||||||
|
|
||||||
|
// now check the population of ranged attributes is OK
|
||||||
|
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
|
||||||
|
GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
|
||||||
|
groupMapper.syncDataFromFederationProviderToKeycloak(appRealm);
|
||||||
|
|
||||||
|
GroupModel kcBigGroup = KeycloakModelUtils.findGroupByPath(appRealm, "/biggroup");
|
||||||
|
// check all the users have the group assigned
|
||||||
|
for (int i = 0; i < membersToTest; i++) {
|
||||||
|
UserModel kcUser = session.users().getUserByUsername(String.format("user%02d", i), appRealm);
|
||||||
|
Assert.assertTrue("User contains biggroup " + i, kcUser.getGroups().contains(kcBigGroup));
|
||||||
|
}
|
||||||
|
// check the group contains all the users as member
|
||||||
|
List<UserModel> groupMembers = session.users().getGroupMembers(appRealm, kcBigGroup, 0, membersToTest);
|
||||||
|
Assert.assertEquals(membersToTest, groupMembers.size());
|
||||||
|
Set<String> usernames = groupMembers.stream().map(u -> u.getUsername()).collect(Collectors.toSet());
|
||||||
|
for (int i = 0; i < membersToTest; i++) {
|
||||||
|
Assert.assertTrue("Group contains user " + i, usernames.contains(String.format("user%02d", i)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test09_emptyMemberOnDeletionWorks() {
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
LDAPTestContext ctx = LDAPTestContext.init(session);
|
||||||
|
RealmModel appRealm = ctx.getRealm();
|
||||||
|
ComponentModel mapperModel = LDAPTestUtils.getSubcomponentByName(appRealm, ctx.getLdapModel(), "groupsMapper");
|
||||||
|
|
||||||
|
// create a group with an existing user alone
|
||||||
|
String descriptionAttrName = getGroupDescriptionLDAPAttrName(ctx.getLdapProvider());
|
||||||
|
LDAPObject deleteGroup = LDAPTestUtils.createLDAPGroup(session, appRealm, ctx.getLdapModel(), "deletegroup", descriptionAttrName, "deletegroup - description");
|
||||||
|
LDAPObject maryLdap = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "marykeycloak");
|
||||||
|
LDAPUtils.addMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, "not-used", deleteGroup, maryLdap);
|
||||||
|
LDAPObject empty = new LDAPObject();
|
||||||
|
empty.setDn(LDAPDn.fromString(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE));
|
||||||
|
LDAPUtils.deleteMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, descriptionAttrName, deleteGroup, empty);
|
||||||
|
deleteGroup = LDAPGroupMapperTest.searchObjectInBase(ctx.getLdapProvider(), deleteGroup.getDn().toString(), LDAPConstants.MEMBER);
|
||||||
|
Assert.assertNotNull(deleteGroup);
|
||||||
|
Assert.assertEquals(1, deleteGroup.getAttributeAsSet(LDAPConstants.MEMBER).size());
|
||||||
|
Assert.assertEquals(maryLdap.getDn(), LDAPDn.fromString(deleteGroup.getAttributeAsString(LDAPConstants.MEMBER)));
|
||||||
|
|
||||||
|
// import into keycloak
|
||||||
|
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
|
||||||
|
GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
|
||||||
|
groupMapper.syncDataFromFederationProviderToKeycloak(appRealm);
|
||||||
|
|
||||||
|
// check everything is OK
|
||||||
|
GroupModel kcDeleteGroup = KeycloakModelUtils.findGroupByPath(appRealm, "/deletegroup");
|
||||||
|
UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
|
||||||
|
List<UserModel> groupMembers = session.users().getGroupMembers(appRealm, kcDeleteGroup, 0, 5);
|
||||||
|
Assert.assertEquals(1, groupMembers.size());
|
||||||
|
Assert.assertEquals("marykeycloak", groupMembers.iterator().next().getUsername());
|
||||||
|
Set<GroupModel> maryGroups = mary.getGroups();
|
||||||
|
Assert.assertEquals(1, maryGroups.size());
|
||||||
|
Assert.assertEquals("deletegroup", maryGroups.iterator().next().getName());
|
||||||
|
|
||||||
|
// delete the group from mary to force schema violation and assingment of the empty value
|
||||||
|
mary.leaveGroup(kcDeleteGroup);
|
||||||
|
|
||||||
|
// check now the group has the empty member instead of mary
|
||||||
|
deleteGroup = LDAPGroupMapperTest.searchObjectInBase(ctx.getLdapProvider(), deleteGroup.getDn().toString(), LDAPConstants.MEMBER);
|
||||||
|
Assert.assertNotNull(deleteGroup);
|
||||||
|
Assert.assertEquals(1, deleteGroup.getAttributeAsSet(LDAPConstants.MEMBER).size());
|
||||||
|
Assert.assertEquals(LDAPDn.fromString(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE), LDAPDn.fromString(deleteGroup.getAttributeAsString(LDAPConstants.MEMBER)));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,8 @@ import org.jboss.logging.Logger;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.directory.server.core.api.interceptor.Interceptor;
|
||||||
|
import org.apache.directory.server.core.normalization.NormalizationInterceptor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for a fast (mostly in-memory-only) ApacheDS DirectoryService. Use only for tests!!
|
* Factory for a fast (mostly in-memory-only) ApacheDS DirectoryService. Use only for tests!!
|
||||||
|
@ -55,6 +57,7 @@ import java.util.List;
|
||||||
class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(InMemoryDirectoryServiceFactory.class);
|
private static final Logger log = Logger.getLogger(InMemoryDirectoryServiceFactory.class);
|
||||||
|
private static final int PAGE_SIZE = 30;
|
||||||
|
|
||||||
private final DirectoryService directoryService;
|
private final DirectoryService directoryService;
|
||||||
private final PartitionFactory partitionFactory;
|
private final PartitionFactory partitionFactory;
|
||||||
|
@ -87,6 +90,7 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public void init(String name) throws Exception {
|
public void init(String name) throws Exception {
|
||||||
if ((directoryService != null) && directoryService.isStarted()) {
|
if ((directoryService != null) && directoryService.isStarted()) {
|
||||||
return;
|
return;
|
||||||
|
@ -144,12 +148,26 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
||||||
systemPartition.setSchemaManager(directoryService.getSchemaManager());
|
systemPartition.setSchemaManager(directoryService.getSchemaManager());
|
||||||
partitionFactory.addIndex(systemPartition, SchemaConstants.OBJECT_CLASS_AT, 100);
|
partitionFactory.addIndex(systemPartition, SchemaConstants.OBJECT_CLASS_AT, 100);
|
||||||
directoryService.setSystemPartition(systemPartition);
|
directoryService.setSystemPartition(systemPartition);
|
||||||
|
|
||||||
|
// Find Normalization interceptor in chain and add our range emulated interceptor
|
||||||
|
List<Interceptor> interceptors = directoryService.getInterceptors();
|
||||||
|
int insertionPosition = -1;
|
||||||
|
for (int pos = 0; pos < interceptors.size(); ++pos) {
|
||||||
|
Interceptor interceptor = interceptors.get(pos);
|
||||||
|
if (interceptor instanceof NormalizationInterceptor) {
|
||||||
|
insertionPosition = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interceptors.add(insertionPosition + 1, new RangedAttributeInterceptor("member", PAGE_SIZE));
|
||||||
|
directoryService.setInterceptors(interceptors);
|
||||||
|
|
||||||
directoryService.startup();
|
directoryService.startup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public DirectoryService getDirectoryService() throws Exception {
|
public DirectoryService getDirectoryService() throws Exception {
|
||||||
return directoryService;
|
return directoryService;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +175,7 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public PartitionFactory getPartitionFactory() throws Exception {
|
public PartitionFactory getPartitionFactory() throws Exception {
|
||||||
return partitionFactory;
|
return partitionFactory;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,258 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.util.ldap;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.apache.directory.api.ldap.model.cursor.ClosureMonitor;
|
||||||
|
import org.apache.directory.api.ldap.model.cursor.CursorException;
|
||||||
|
import org.apache.directory.api.ldap.model.entry.Attribute;
|
||||||
|
import org.apache.directory.api.ldap.model.entry.Entry;
|
||||||
|
import org.apache.directory.api.ldap.model.entry.Value;
|
||||||
|
import org.apache.directory.api.ldap.model.exception.LdapException;
|
||||||
|
import org.apache.directory.api.ldap.model.schema.AttributeType;
|
||||||
|
import org.apache.directory.api.ldap.model.schema.AttributeTypeOptions;
|
||||||
|
import org.apache.directory.server.core.api.filtering.EntryFilter;
|
||||||
|
import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
|
||||||
|
import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
|
||||||
|
import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Ranged interceptor to emulate the behavior of AD. AD has a limit in
|
||||||
|
* the number of attributes that return (15000 by default in MaxValRange).
|
||||||
|
* See this MS link for AD limits:</p>
|
||||||
|
*
|
||||||
|
* https://support.microsoft.com/en-us/help/315071/how-to-view-and-set-ldap-policy-in-active-directory-by-using-ntdsutil
|
||||||
|
*
|
||||||
|
* <p>And this other link to know how range attribute search works:</p>
|
||||||
|
*
|
||||||
|
* https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ldap/searching-using-range-retrieval
|
||||||
|
*
|
||||||
|
* @author rmartinc
|
||||||
|
*/
|
||||||
|
public class RangedAttributeInterceptor extends BaseInterceptor {
|
||||||
|
|
||||||
|
private class RangedEntryFilteringCursor implements EntryFilteringCursor {
|
||||||
|
|
||||||
|
private final EntryFilteringCursor c;
|
||||||
|
private final String name;
|
||||||
|
private final Integer min;
|
||||||
|
private final Integer max;
|
||||||
|
|
||||||
|
public RangedEntryFilteringCursor(EntryFilteringCursor c, String name, Integer min, Integer max) {
|
||||||
|
this.c = c;
|
||||||
|
this.name = name;
|
||||||
|
this.min = min;
|
||||||
|
this.max = max;
|
||||||
|
AttributeType type = new AttributeType(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Entry prepareEntry(Entry e) {
|
||||||
|
Attribute attr = e.get(name);
|
||||||
|
if (attr != null) {
|
||||||
|
int start = (min != null)? min : 0;
|
||||||
|
start = (start < attr.size())? start : attr.size() - 1;
|
||||||
|
int end = (max != null && max < attr.size() - 1)? max : attr.size() - 1;
|
||||||
|
if (start != 0 || end != attr.size() - 1) {
|
||||||
|
// some values should be stripped out
|
||||||
|
Iterator<Value<?>> it = attr.iterator();
|
||||||
|
Set<Value<?>> valuesToRemove = new HashSet<>(end - start + 1);
|
||||||
|
for (int i = 0; i < attr.size(); i++) {
|
||||||
|
Value<?> v = it.next();
|
||||||
|
if (i < start || i > end) {
|
||||||
|
valuesToRemove.add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attr.setUpId(attr.getUpId() + ";range=" + start + "-" + ((end == attr.size() - 1)? "*" : end));
|
||||||
|
attr.remove(valuesToRemove.toArray(new Value<?>[0]));
|
||||||
|
} else if (min != null) {
|
||||||
|
// range explicitly requested although no value stripped
|
||||||
|
attr.setUpId(attr.getUpId() + ";range=0-*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addEntryFilter(EntryFilter ef) {
|
||||||
|
return c.addEntryFilter(ef);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EntryFilter> getEntryFilters() {
|
||||||
|
return c.getEntryFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchOperationContext getOperationContext() {
|
||||||
|
return c.getOperationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean available() {
|
||||||
|
return c.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void before(Entry e) throws LdapException, CursorException {
|
||||||
|
c.before(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void after(Entry e) throws LdapException, CursorException {
|
||||||
|
c.after(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeFirst() throws LdapException, CursorException {
|
||||||
|
c.beforeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterLast() throws LdapException, CursorException {
|
||||||
|
c.afterLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean first() throws LdapException, CursorException {
|
||||||
|
return c.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFirst() {
|
||||||
|
return c.isFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBeforeFirst() {
|
||||||
|
return c.isBeforeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean last() throws LdapException, CursorException {
|
||||||
|
return c.last();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLast() {
|
||||||
|
return c.isLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAfterLast() {
|
||||||
|
return c.isAfterLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
|
return c.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean previous() throws LdapException, CursorException {
|
||||||
|
return c.previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean next() throws LdapException, CursorException {
|
||||||
|
return c.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Entry get() throws CursorException {
|
||||||
|
return prepareEntry(c.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
c.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close(Exception excptn) {
|
||||||
|
c.close(excptn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClosureMonitor(ClosureMonitor cm) {
|
||||||
|
c.setClosureMonitor(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(String string) {
|
||||||
|
return c.toString(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Entry> iterator() {
|
||||||
|
return c.iterator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final int max;
|
||||||
|
|
||||||
|
public RangedAttributeInterceptor(String name, int max) {
|
||||||
|
this.name = name;
|
||||||
|
this.max = max - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntryFilteringCursor search(SearchOperationContext sc) throws LdapException {
|
||||||
|
Set<AttributeTypeOptions> attrs = sc.getReturningAttributes();
|
||||||
|
Integer lmin = null, lmax = max;
|
||||||
|
if (attrs != null) {
|
||||||
|
for (AttributeTypeOptions attr : attrs) {
|
||||||
|
if (attr.getAttributeType().getName().equalsIgnoreCase(name)) {
|
||||||
|
if (attr.getOptions() != null) {
|
||||||
|
for (String option : attr.getOptions()) {
|
||||||
|
if (option.startsWith("range=")) {
|
||||||
|
String[] ranges = option.substring(6).split("-");
|
||||||
|
if (ranges.length == 2) {
|
||||||
|
try {
|
||||||
|
lmin = Integer.parseInt(ranges[0]);
|
||||||
|
if (lmin < 0) {
|
||||||
|
lmin = 0;
|
||||||
|
}
|
||||||
|
if ("*".equals(ranges[1])) {
|
||||||
|
lmax = lmin + max;
|
||||||
|
} else {
|
||||||
|
lmax = Integer.parseInt(ranges[1]);
|
||||||
|
if (lmax < lmin) {
|
||||||
|
lmax = lmin;
|
||||||
|
} else if (lmax > lmin + max) {
|
||||||
|
lmax = lmin + max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
lmin = null;
|
||||||
|
lmax = max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RangedEntryFilteringCursor(super.next(sc), name, lmin, lmax);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue