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.Map;
|
||||
import java.util.Set;
|
||||
import javax.naming.directory.SearchControls;
|
||||
|
||||
/**
|
||||
* 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 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(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
|
||||
String membership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
|
||||
|
||||
memberships.add(membership);
|
||||
ldapParent.setAttribute(memberAttrName, memberships);
|
||||
|
||||
if (sendLDAPUpdateRequest) {
|
||||
ldapProvider.getLdapIdentityStore().update(ldapParent);
|
||||
}
|
||||
ldapProvider.getLdapIdentityStore().addMemberToGroup(ldapParent.getDn().toString(), memberAttrName, membership);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,29 +185,20 @@ public class LDAPUtils {
|
|||
* @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) {
|
||||
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
|
||||
|
||||
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
|
||||
|
||||
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);
|
||||
ldapProvider.getLdapIdentityStore().removeMemberFromGroup(ldapParent.getDn().toString(), memberAttrName, userMembership);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all existing memberships (values of attribute 'member' ) from the given ldapRole or ldapGroup
|
||||
*
|
||||
* @param ldapProvider The ldap provider
|
||||
* @param memberAttrName usually 'member'
|
||||
* @param ldapRole
|
||||
* @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);
|
||||
if (memberships == null) {
|
||||
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
|
||||
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() {
|
||||
return uuid;
|
||||
|
@ -123,6 +125,36 @@ public class LDAPObject {
|
|||
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() {
|
||||
return attributes;
|
||||
|
@ -152,6 +184,7 @@ public class LDAPObject {
|
|||
|
||||
@Override
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
List<LDAPObject> fetchQueryResults(LDAPQuery LDAPQuery);
|
||||
|
|
|
@ -54,6 +54,11 @@ import java.util.Map;
|
|||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
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
|
||||
|
@ -65,6 +70,7 @@ import java.util.TreeSet;
|
|||
public class LDAPIdentityStore implements IdentityStore {
|
||||
|
||||
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 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
|
||||
public void update(LDAPObject ldapObject) {
|
||||
checkRename(ldapObject);
|
||||
|
@ -199,7 +243,8 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -351,6 +396,21 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
|
||||
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())) {
|
||||
Object uuidValue = ldapAttribute.get();
|
||||
ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
|
||||
|
|
|
@ -528,50 +528,52 @@ public class LDAPOperationManager {
|
|||
}
|
||||
}
|
||||
|
||||
public void modifyAttributes(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) {
|
||||
try {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Modifying attributes for entry [%s]: [", dn);
|
||||
public void modifyAttributesNaming(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) throws NamingException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Modifying attributes for entry [%s]: [", dn);
|
||||
|
||||
for (ModificationItem item : mods) {
|
||||
Object values;
|
||||
for (ModificationItem item : mods) {
|
||||
Object values;
|
||||
|
||||
if (item.getAttribute().size() > 0) {
|
||||
values = item.getAttribute().get();
|
||||
} else {
|
||||
values = "No values";
|
||||
}
|
||||
|
||||
String attrName = item.getAttribute().getID().toUpperCase();
|
||||
if (attrName.contains("PASSWORD") || attrName.contains("UNICODEPWD")) {
|
||||
values = "********************";
|
||||
}
|
||||
|
||||
logger.tracef(" Op [%s]: %s = %s", item.getModificationOp(), item.getAttribute().getID(), values);
|
||||
if (item.getAttribute().size() > 0) {
|
||||
values = item.getAttribute().get();
|
||||
} else {
|
||||
values = "No values";
|
||||
}
|
||||
|
||||
logger.tracef("]");
|
||||
String attrName = item.getAttribute().getID().toUpperCase();
|
||||
if (attrName.contains("PASSWORD") || attrName.contains("UNICODEPWD")) {
|
||||
values = "********************";
|
||||
}
|
||||
|
||||
logger.tracef(" Op [%s]: %s = %s", item.getModificationOp(), item.getAttribute().getID(), values);
|
||||
}
|
||||
|
||||
execute(new LdapOperation<Void>() {
|
||||
logger.tracef("]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void execute(LdapContext context) throws NamingException {
|
||||
context.modifyAttributes(new LdapName(dn), mods);
|
||||
return null;
|
||||
}
|
||||
execute(new LdapOperation<Void>() {
|
||||
|
||||
@Override
|
||||
public Void execute(LdapContext context) throws NamingException {
|
||||
context.modifyAttributes(new LdapName(dn), mods);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("LdapOperation: modify\n")
|
||||
.append(" dn: ").append(dn).append("\n")
|
||||
.append(" modificationsSize: ").append(mods.length)
|
||||
.toString();
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("LdapOperation: modify\n")
|
||||
.append(" dn: ").append(dn).append("\n")
|
||||
.append(" modificationsSize: ").append(mods.length)
|
||||
.toString();
|
||||
}
|
||||
|
||||
}, null, decorator);
|
||||
}
|
||||
|
||||
}, null, decorator);
|
||||
public void modifyAttributes(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) {
|
||||
try {
|
||||
modifyAttributesNaming(dn, mods, decorator);
|
||||
} catch (NamingException e) {
|
||||
throw new ModelException("Could not modify attribute for DN [" + dn + "]", e);
|
||||
}
|
||||
|
|
|
@ -50,12 +50,12 @@ public enum MembershipType {
|
|||
@Override
|
||||
public Set<LDAPDn> getLDAPSubgroups(GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup) {
|
||||
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"
|
||||
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
|
||||
Set<String> allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup);
|
||||
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPStorageProvider ldapProvider, LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
|
||||
Set<String> allMemberships = LDAPUtils.getExistingMemberships(ldapProvider, membershipLdapAttribute, ldapGroup);
|
||||
|
||||
// Filter and keep just descendants of requiredParentDn
|
||||
Set<LDAPDn> result = new HashSet<>();
|
||||
|
@ -74,7 +74,7 @@ public enum MembershipType {
|
|||
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
|
||||
|
||||
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) {
|
||||
return Collections.emptyList();
|
||||
|
@ -139,7 +139,7 @@ public enum MembershipType {
|
|||
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
|
||||
|
||||
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) {
|
||||
return Collections.emptyList();
|
||||
|
|
|
@ -465,8 +465,10 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
|||
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
|
||||
for (GroupModel kcSubgroup : kcSubgroups) {
|
||||
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
|
||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapSubgroup, false);
|
||||
toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn());
|
||||
if (!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
|
||||
|
@ -476,11 +478,6 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
|||
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()) {
|
||||
processKeycloakGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
|
||||
}
|
||||
|
@ -510,6 +507,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
|||
|
||||
@Override
|
||||
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());
|
||||
if (ldapGroup == null) {
|
||||
return Collections.emptyList();
|
||||
|
@ -539,7 +537,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
|||
// Finally update LDAP membership in the parent group
|
||||
if (highestGroupToSync.getParent() != null) {
|
||||
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 {
|
||||
// No care about group inheritance. Let's just sync current group
|
||||
|
@ -551,7 +549,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
|||
|
||||
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) {
|
||||
|
|
|
@ -261,7 +261,7 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
|||
|
||||
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) {
|
||||
|
|
|
@ -142,17 +142,17 @@ public class TestLDAPResource {
|
|||
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");
|
||||
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11, false);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group12, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11);
|
||||
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, defaultGroup12, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, teamChild20182019, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamChild20182019, teamSubChild20202021, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup13, teamSubChild20222023, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamSubChild20222023, defaultGroup14, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamRoot20242025, defaultGroup15, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup15, teamSubChild20262027, true);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup11);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, defaultGroup12);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup1, teamChild20182019);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamChild20182019, teamSubChild20202021);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup13, teamSubChild20222023);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamSubChild20222023, defaultGroup14);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", teamRoot20242025, defaultGroup15);
|
||||
LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", defaultGroup15, teamSubChild20262027);
|
||||
|
||||
// Sync LDAP groups to Keycloak DB
|
||||
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 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, group12, true);
|
||||
LDAPUtils.addMember(ctx.getLdapProvider(), MembershipType.DN, LDAPConstants.MEMBER, "not-used", group1, group11);
|
||||
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
|
||||
LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
|
||||
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 {
|
||||
new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(realm);
|
||||
|
@ -298,7 +298,7 @@ public class LDAPGroupMapperSyncTest extends AbstractLDAPTest {
|
|||
GroupMapperConfig groupMapperConfig = new GroupMapperConfig(mapperModel);
|
||||
LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName("group11");
|
||||
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.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
|
||||
|
|
|
@ -19,6 +19,8 @@ package org.keycloak.testsuite.federation.ldap;
|
|||
|
||||
import java.util.List;
|
||||
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.TargetsContainer;
|
||||
|
@ -45,6 +47,7 @@ import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper
|
|||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
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.testsuite.runonserver.RunOnServerDeployment;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
|
@ -476,14 +479,14 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
|
|||
|
||||
// 2 - Add one existing user rob to LDAP group
|
||||
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
|
||||
LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
||||
nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent");
|
||||
LDAPObject nonExistentLdapUser = new LDAPObject();
|
||||
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
|
||||
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.IOException;
|
||||
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!!
|
||||
|
@ -55,6 +57,7 @@ import java.util.List;
|
|||
class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InMemoryDirectoryServiceFactory.class);
|
||||
private static final int PAGE_SIZE = 30;
|
||||
|
||||
private final DirectoryService directoryService;
|
||||
private final PartitionFactory partitionFactory;
|
||||
|
@ -87,6 +90,7 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
|||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void init(String name) throws Exception {
|
||||
if ((directoryService != null) && directoryService.isStarted()) {
|
||||
return;
|
||||
|
@ -144,12 +148,26 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
|||
systemPartition.setSchemaManager(directoryService.getSchemaManager());
|
||||
partitionFactory.addIndex(systemPartition, SchemaConstants.OBJECT_CLASS_AT, 100);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public DirectoryService getDirectoryService() throws Exception {
|
||||
return directoryService;
|
||||
}
|
||||
|
@ -157,6 +175,7 @@ class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory {
|
|||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public PartitionFactory getPartitionFactory() throws Exception {
|
||||
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