diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java index 03107f8696..563e4f231b 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java @@ -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 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 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 getExistingMemberships(String memberAttrName, LDAPObject ldapRole) { + public static Set getExistingMemberships(LDAPStorageProvider ldapProvider, String memberAttrName, LDAPObject ldapRole) { + LDAPUtils.fillRangedAttribute(ldapProvider, ldapRole, memberAttrName); Set 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); + } + } } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPObject.java index 64ef65fd07..2862a0571e 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPObject.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/model/LDAPObject.java @@ -48,6 +48,8 @@ public class LDAPObject { // Copy of "attributes" containing lower-cased keys private final Map> lowerCasedAttributes = new HashMap<>(); + // range attributes are always read from 0 to max so just saving the top value + private final Map 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 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> 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 + " ]"; } } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java index 5a57d28e72..04a6ce8d2a 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/IdentityStore.java @@ -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 fetchQueryResults(LDAPQuery LDAPQuery); diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java index 1a9f7c071b..c43ebc3545 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -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)); diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java index 2edfd17a0a..1548a36df8 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java @@ -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() { + logger.tracef("]"); + } - @Override - public Void execute(LdapContext context) throws NamingException { - context.modifyAttributes(new LdapName(dn), mods); - return null; - } + execute(new LdapOperation() { + @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); } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java index 894b2b4d44..b9c7844585 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/MembershipType.java @@ -50,12 +50,12 @@ public enum MembershipType { @Override public Set 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 getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) { - Set allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup); + protected Set getLDAPMembersWithParent(LDAPStorageProvider ldapProvider, LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) { + Set allMemberships = LDAPUtils.getExistingMemberships(ldapProvider, membershipLdapAttribute, ldapGroup); // Filter and keep just descendants of requiredParentDn Set result = new HashSet<>(); @@ -74,7 +74,7 @@ public enum MembershipType { CommonLDAPGroupMapperConfig config = groupMapper.getConfig(); LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); - Set userDns = getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), usersDn); + Set 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 memberUids = LDAPUtils.getExistingMemberships(memberAttrName, ldapGroup); + Set memberUids = LDAPUtils.getExistingMemberships(ldapProvider, memberAttrName, ldapGroup); if (memberUids == null || memberUids.size() <= firstResult) { return Collections.emptyList(); diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java index d446fbdf7b..bd014f360c 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapper.java @@ -465,8 +465,10 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements Set 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 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) { diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java index ee7771530f..9f9607a228 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java @@ -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) { diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java index 246b2e0443..8807b0bf19 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestLDAPResource.java @@ -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"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperSyncTest.java index 25e783cbe0..14025979c6 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperSyncTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperSyncTest.java @@ -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")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java index 76b227b69b..e52eb368f9 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java @@ -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 groupMembers = session.users().getGroupMembers(appRealm, kcBigGroup, 0, membersToTest); + Assert.assertEquals(membersToTest, groupMembers.size()); + Set 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 groupMembers = session.users().getGroupMembers(appRealm, kcDeleteGroup, 0, 5); + Assert.assertEquals(1, groupMembers.size()); + Assert.assertEquals("marykeycloak", groupMembers.iterator().next().getUsername()); + Set 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))); + }); + } } diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java index 649030a8ae..2b324694ca 100644 --- a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/InMemoryDirectoryServiceFactory.java @@ -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 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; } diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/RangedAttributeInterceptor.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/RangedAttributeInterceptor.java new file mode 100644 index 0000000000..d7f47714fa --- /dev/null +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/RangedAttributeInterceptor.java @@ -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; + +/** + *

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:

+ * + * https://support.microsoft.com/en-us/help/315071/how-to-view-and-set-ldap-policy-in-active-directory-by-using-ntdsutil + * + *

And this other link to know how range attribute search works:

+ * + * 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> it = attr.iterator(); + Set> 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 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 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 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); + } +} \ No newline at end of file