Uploaded image for project: 'Crowd Data Center'
  1. Crowd Data Center
  2. CWD-2762

Support changing active status (activating/deactivating) for users in OpenLDAP directories

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: Medium Medium
    • None
    • 2.0.7, 2.4, 2.8.3, 2.8.4, 2.10.1
    • Crowd with LDAP as backend, LDAP administration by Collax Businness Server

      When we change in Collax a user to "inactive", this state is not reflected in the crowd database.
      The proposed workaround is to remove the user from ldap and re-create the user on crowd. The result is two directories of users in one company. Administration, search, migration will become error-prone and is not acceptable for us. After using crowd with LDAP for about 2 yours, we get to our licence-limit of 100, where the active users are about 70 users.

      Steps to reproduce:

      1. install Crowd
      2. Create new Directory connecting to OpenLDAP using Connector
      3. try to disable any user sync from OpenLDAP by unchecking the Active checkbox

      Expected Behaviour:
      User will be disactivated

      Actual Behaviour:
      Crowd indicate update is sucessful, but the user remain active

      Environment:
      Crowd connected to OpenLDAP

      Notes
      This is not reproducible when Crowd is connecting to Microsoft AD

        1. CWD-2762-2.11.1.patch
          25 kB
        2. CWD-2762-3.1.2.patch
          33 kB
        3. CWD-2762-for-2.9.1.diff
          23 kB
        4. CWD-2762-version1.diff
          25 kB
        5. screenshot2.png
          screenshot2.png
          70 kB

            [CWD-2762] Support changing active status (activating/deactivating) for users in OpenLDAP directories

            Below the patch for Crowd 4.1.2

            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -1,16 +1,30 @@
             package com.atlassian.crowd.directory;
             
            +import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper;
             import com.atlassian.crowd.directory.ldap.credential.EncryptingCredentialEncoder;
             import com.atlassian.crowd.directory.ldap.credential.LDAPCredentialEncoder;
            +import com.atlassian.crowd.directory.ldap.mapper.UserContextMapperConfig;
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper;
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.user.UserActiveMapper;
             import com.atlassian.crowd.model.user.User;
             import com.atlassian.crowd.password.factory.PasswordEncoderFactory;
             import com.atlassian.crowd.search.ldap.LDAPQueryTranslater;
             import com.atlassian.crowd.util.InstanceFactory;
             import com.atlassian.event.api.EventPublisher;
            +import com.google.common.collect.ImmutableList;
            +import com.google.common.collect.ImmutableList.Builder;
             
             import javax.naming.directory.Attributes;
            +import java.util.HashMap;
            +import java.util.List;
            +import java.util.Map;
             
             public class OpenLDAP extends RFC4519Directory {
            +
            +    private static final String ATTRIBUTE_LOCKOUT_KEY = "pwdAccountLockedTime";
            +    /** @see <a href="http://linux.die.net/man/5/slapo-ppolicy">man for slapo-ppolicy</a> */
            +    private static final String ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED = "000001010000Z";
            +
                 private final PasswordEncoderFactory passwordEncoderFactory;
             
                 public OpenLDAP(LDAPQueryTranslater ldapQueryTranslater, EventPublisher eventPublisher, InstanceFactory instanceFactory,
            @@ -25,7 +39,7 @@
             
                 @Override
                 public String getDescriptiveName() {
            -        return OpenLDAP.getStaticDirectoryType();
            +        return OpenLDAP.getStaticDirectoryType() + " (extended with support for sync of active flag, CWD-2762)";
                 }
             
                 @Override
            @@ -43,4 +57,31 @@
                 protected void getNewUserDirectorySpecificAttributes(final User user, final Attributes attributes) {
                     addDefaultSnToUserAttributes(attributes, user.getName());
                 }
            +
            +
            +    /**
            +     * As long as the LDAP attributes and values are not configurable, we just add those as hard-coded attributes in here, that they are available within {@link com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper}
            +     * The LDAP attribute keys and values are taken from the de-facto standard "Password Policy for LDAP Directories"
            +     * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy-10">Password Policy for LDAP Directories</a>
            +     * @see <a href="http://linux.die.net/man/5/slapo-ppolicy">man for slapo-ppolicy</a>
            +     */
            +    @Override
            +    public void setAttributes(Map<String, String> attributes) {
            +        Map<String, String> extendedAttributes = new HashMap<String, String>(attributes);
            +
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_ATTRIBUTE_KEY, ATTRIBUTE_LOCKOUT_KEY);
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_VALUE, "");
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_INACTIVE_VALUE, ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED);
            +
            +        super.setAttributes(extendedAttributes);
            +    }
            +
            +    @Override
            +    protected List<AttributeMapper> getCustomUserAttributeMappers(UserContextMapperConfig config) {
            +
            +        Builder<AttributeMapper> builder = ImmutableList.<AttributeMapper>builder();
            +        builder.addAll(super.getCustomUserAttributeMappers(config));
            +        builder.add(new UserActiveMapper(ldapPropertiesMapper.getUserActiveAttribute()));
            +        return builder.build();
            +    }
             }
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -870,6 +870,10 @@
             
                         try {
                             SearchControls searchControls = getSearchControls(contextMapper, SearchControls.OBJECT_SCOPE);
            +                if (contextMapper instanceof ContextMapperWithRequiredAttributes<?>) {
            +                    logger.debug("Return required attributes {} for findEntityById", (((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes()));
            +                    searchControls.setReturningAttributes(toArray(((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes()));
            +                }
                             searchControls.setTimeLimit(ldapPropertiesMapper.getSearchTimeLimit());
                             entities = ldapTemplate.search(asLdapName(dn, "DN: " + dn, entityClass), filter, searchControls, contextMapper);
                         } catch (NameNotFoundException e) {
            @@ -971,6 +975,30 @@
                         modificationItems.add(displayNameMod);
                     }
             
            +        // support enabling/disabling users
            +        if (supportsInactiveAccounts()) {
            +            ModificationItem activeModItem = null;
            +            if (!currentUser.isActive() && userTemplate.isActive()) {
            +                // try to activate user in case it is currently inactive
            +                String newValue = ldapPropertiesMapper.getUserActiveValue();
            +                if (StringUtils.isEmpty(newValue)) {
            +                    // rather than modifying an attribute we completely remove it
            +                    activeModItem = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute(ldapPropertiesMapper.getUserActiveAttribute()));
            +                } else {
            +                    // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value
            +                    activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed", newValue);
            +                }
            +            } else if (currentUser.isActive() && !userTemplate.isActive()) {
            +                // try to deactivate user in case it is currently active
            +                String newValue = ldapPropertiesMapper.getUserInactiveValue();
            +                // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value
            +                activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed", newValue);
            +            }
            +            if (activeModItem != null)
            +            {
            +                modificationItems.add(activeModItem);
            +            }
            +        }
                     return modificationItems;
                 }
             
            @@ -1545,7 +1573,7 @@
                  */
                 @Override
                 public boolean supportsInactiveAccounts() {
            -        return false;
            +        return StringUtils.isNotBlank(ldapPropertiesMapper.getUserActiveAttribute());
                 }
             
                 @Override
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -176,6 +176,21 @@
                  */
                 String USER_PASSWORD_KEY = "ldap.user.password";
             
            +    /**
            +     * Attribute key for the LDAP principal active attribute.
            +     */
            +    String USER_ACTIVE_ATTRIBUTE_KEY = "ldap.user.active.attribute";
            +
            +    /**
            +     * Attribute key for the LDAP value which stands for active users. In contrast to {@link #USER_INACTIVE_VALUE} this is only considered for writing to the LDAP, not for evaluation!
            +     */
            +    String USER_ACTIVE_VALUE = "ldap.user.active";
            +
            +    /**
            +     * Attribute key for the LDAP value which stands for inactive users. This is used for checking whether an account is active and also for persisting that information within the LDAP.
            +     */
            +    String USER_INACTIVE_VALUE = "ldap.user.inactive";
            +
                 /**
                  * Attribute key for the LDAP paged results attribute.
                  */
            @@ -364,6 +379,22 @@
             
                 String getUserEncryptionMethod();
             
            +    /**
            +     * @return the LDAP attribute in which the users active state is stored. Derived from {@link #USER_ACTIVE_ATTRIBUTE_KEY}.
            +     */
            +    String getUserActiveAttribute();
            +
            +    /**
            +     * @return the LDAP value which stands for an active user (should only be used for turning a user from inactive to active, i.e. for writing to LDAP, but not for evaluating the state). Derived from {@link #USER_ACTIVE_VALUE}}
            +     */
            +    String getUserActiveValue();
            +
            +    /**
            +     * @return the LDAP value which stands for an inactive user (should only be used for turning a user from active to inactive, i.e. for writing to LDAP, as well as for evaluating the current state). Derived from {@link #USER_INACTIVE_VALUE}}
            +     */
            +    String getUserInactiveValue();
            +
            +
                 boolean isPagedResultsControl();
             
                 int getPagedResultsSize();
            @@ -415,7 +446,7 @@
                  * compatible method for standardising DNs when mapping object DNs and
                  * and memberDNs (value = <code>false</code>); or if we can use a more
                  * efficient but relaxed form of standardisation (value = <code>true</code>).
            -     *
            +     * <p>
                  * See <code>DNStandardiser</code> for more information.
                  *
                  * @return <code>false</code> if proper standardisation is required.
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -419,6 +419,22 @@
                     return getAttribute(LDAP_USER_ENCRYPTION_METHOD);
                 }
             
            +    @Override
            +    public String getUserActiveAttribute()
            +    {
            +    	return getAttribute(USER_ACTIVE_ATTRIBUTE_KEY);
            +    }
            +
            +    @Override
            +    public String getUserActiveValue() {
            +    	return getAttribute(USER_ACTIVE_VALUE);
            +    }
            +
            +    @Override
            +    public String getUserInactiveValue() {
            +    	return getAttribute(USER_INACTIVE_VALUE);
            +    }
            +
                 /**
                  * Checks if the configuration of the LDAP directory server uses paged results.
                  *
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -0,0 +1,38 @@
            +package com.atlassian.crowd.directory.ldap.mapper.attribute.user;
            +
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper;
            +import org.springframework.ldap.core.DirContextAdapter;
            +
            +import java.util.Collections;
            +import java.util.Set;
            +
            +public class UserActiveMapper implements AttributeMapper {
            +
            +    private final String userActiveAttribute;
            +
            +    public UserActiveMapper(String userActiveAttribute) {
            +        this.userActiveAttribute = userActiveAttribute;
            +    }
            +
            +    @Override
            +    public String getKey() {
            +        return userActiveAttribute;
            +    }
            +
            +    @Override
            +    public Set<String> getValues(DirContextAdapter ctx) throws Exception {
            +        // this must never return null
            +        String userActiveValue = ctx.getStringAttribute(getKey());
            +        if (userActiveValue != null) {
            +            return Collections.singleton(userActiveValue);
            +        } else {
            +            return Collections.emptySet();
            +        }
            +    }
            +
            +    @Override
            +    public Set<String> getRequiredLdapAttributes() {
            +        return Collections.singleton(getKey());
            +    }
            +
            +}
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -68,6 +68,13 @@
                     // full name
                     putValueInAttributes(populatedUser.getDisplayName(), ldapPropertiesMapper.getUserDisplayNameAttribute(), directoryAttributes);
             
            +        // active is the default case for LDAP, we only need to write something to the LDAP in case the user is not active
            +        if (!populatedUser.isActive()) {
            +            putValueInAttributes(ldapPropertiesMapper.getUserInactiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        } else {
            +            putValueInAttributes(ldapPropertiesMapper.getUserActiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        }
            +
                     // TODO: currently we don't support arbitrary attributes / iconLocation / active
             
                     return directoryAttributes;
            @@ -129,6 +136,22 @@
                 }
             
                 protected boolean getUserActiveFromAttribute(final Attributes directoryAttributes) {
            +        String userActive = DirectoryAttributeRetriever.getValueFromAttributes(ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        if (userActive == null) {
            +            if (logger.isDebugEnabled()) {
            +                logger.debug("User active attribute '{}' on user '{}' is not set or could not be evaluated, assuming user is active", ldapPropertiesMapper.getUserActiveAttribute(), getUsernameFromAttributes(directoryAttributes));
            +            }
            +            return true;
            +        }
            +
            +        if (logger.isDebugEnabled()) {
            +            logger.debug("User active attribute '{}' is set to '{}' on user {}", ldapPropertiesMapper.getUserActiveAttribute(), userActive, getUsernameFromAttributes(directoryAttributes));
            +        }
            +        // is user inactive?
            +        if (userActive.equals(ldapPropertiesMapper.getUserInactiveValue())) {
            +            logger.debug("User is inactive");
            +            return false;
            +        }
                     return true;
                 }
             
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -5,6 +5,7 @@
             
             import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper;
             import com.atlassian.crowd.embedded.api.SearchRestriction;
            +import com.atlassian.crowd.search.Entity;
             import com.atlassian.crowd.search.EntityDescriptor;
             import com.atlassian.crowd.search.query.entity.EntityQuery;
             import com.atlassian.crowd.search.query.entity.restriction.BooleanRestriction;
            @@ -13,12 +14,13 @@
             import com.atlassian.crowd.search.query.entity.restriction.PropertyRestriction;
             import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys;
             import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys;
            -
            +import org.apache.commons.lang3.StringUtils;
             import org.springframework.ldap.filter.AndFilter;
             import org.springframework.ldap.filter.BinaryLogicalFilter;
             import org.springframework.ldap.filter.EqualsFilter;
             import org.springframework.ldap.filter.Filter;
             import org.springframework.ldap.filter.LikeFilter;
            +import org.springframework.ldap.filter.NotFilter;
             import org.springframework.ldap.filter.NotPresentFilter;
             import org.springframework.ldap.filter.OrFilter;
             import org.springframework.ldap.filter.PresentFilter;
            @@ -127,16 +129,35 @@
             
                 protected Filter booleanTermRestrictionAsFilter(final EntityDescriptor entityDescriptor, final PropertyRestriction<Boolean> termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) {
                     // if boolean term restrictions are for anything other than the group/user active flag, then throw exception
            -        if (!termRestriction.getProperty().equals(GroupTermKeys.ACTIVE) && !termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) {
            -            throw new IllegalArgumentException("Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported");
            -        } else {
            +        if (entityDescriptor.getEntityType() == Entity.USER && termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) {
            +            if (StringUtils.isEmpty(ldapPropertiesMapper.getUserActiveAttribute())) {
            +                if (termRestriction.getValue()) {
            +                    // groups are always active = true, so no need to add a restriction
            +                    return new EverythingResult();
            +                } else {
            +                    // no group is ever active = false, so no results
            +                    return new NothingResult();
            +                }
            +            } else {
            +                if (termRestriction.getValue()) {
            +                    // querying for enabled users
            +                    return new NotFilter(new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue()));
            +                } else {
            +                    // querying for disabled users
            +                    return new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue());
            +                }
            +            }
            +        } else if (entityDescriptor.getEntityType() == Entity.GROUP && termRestriction.getProperty().equals(GroupTermKeys.ACTIVE)) {
                         if (termRestriction.getValue()) {
            -                // everything is active = true, so no need to add a restriction
            +                // groups are always active = true, so no need to add a restriction
                             return new EverythingResult();
                         } else {
            -                // nothing is active = false, so need to
            +                // no group is ever active = false, so no results
                             return new NothingResult();
                         }
            +        } else {
            +            // if boolean term restrictions are for anything other than the group/user active flag, then throw exception
            +            throw new IllegalArgumentException("Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported");
                     }
                 }
             
            Index: atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -24,6 +24,11 @@
             ldap.user.group         memberOf
             ldap.user.password      userPassword
             
            +# active properties (according to http://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
            +ldap.user.active.attribute	pwdAccountLockedTime
            +ldap.user.active
            +ldap.user.inactive		000001010000Z
            +
             # generic options
             ldap.pagedresults               false
             ldap.relaxed.dn.standardisation true
            Index: atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java	(revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650)
            +++ atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java	(revision 840555138e78f913be9c598bf612e68009847b62)
            @@ -13,8 +13,8 @@
             import com.atlassian.crowd.search.query.entity.restriction.PropertyImpl;
             import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys;
             import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys;
            -
             import org.junit.Before;
            +import org.junit.Ignore;
             import org.junit.Test;
             
             import static org.junit.Assert.assertEquals;
            @@ -48,6 +48,11 @@
                     when(ldapPropertiesMapper.getGroupNameAttribute()).thenReturn("cn");
                     when(ldapPropertiesMapper.getGroupDescriptionAttribute()).thenReturn("description");
                     when(ldapPropertiesMapper.getGroupMemberAttribute()).thenReturn("member");
            +
            +        // test user activation through LDAP Password Policy (http://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
            +        when(ldapPropertiesMapper.getUserActiveAttribute()).thenReturn("pwdAccountLockedTime");
            +        when(ldapPropertiesMapper.getUserActiveValue()).thenReturn("");
            +        when(ldapPropertiesMapper.getUserInactiveValue()).thenReturn("000001010000Z");
                 }
             
                 @Test
            @@ -104,7 +109,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(givenName=joe*)(sn=joe*)(displayName=joe*)))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(givenName=joe*)(sn=joe*)(displayName=joe*))(!(pwdAccountLockedTime=000001010000Z))))", filter);
                 }
             
                 @Test
            @@ -116,14 +121,16 @@
                     assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))", filter);
                 }
             
            -    @Test(expected = NullResultException.class)
            +    @Test
                 public void testSearchInactiveUsers() throws NullResultException {
                     EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching(false)).returningAtMost(10);
             
            -        queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        String filter = ldapQuery.encode();
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(pwdAccountLockedTime=000001010000Z))", filter);
                 }
             
            -    @Test(expected = NullResultException.class)
            +    @Test
                 public void testSearchInactiveUsersViaNestingReturningNullResult() throws NullResultException {
                     EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.anyOf(
                             Combine.allOf(
            @@ -136,7 +143,11 @@
                             )
                     )).returningAtMost(10);
             
            -        queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        String filter = ldapQuery.encode();
            +        //assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(pwdAccountLockedTime=000001010000Z)(givenName=b*))))", filter);
            +
                 }
             
                 @Test
            @@ -155,7 +166,8 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(objectCategory=Person)(sAMAccountName=*))", filter);
            +//        assertEquals("(&(objectCategory=Person)(sAMAccountName=*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(!(pwdAccountLockedTime=000001010000Z))(displayName=b*))(|(!(pwdAccountLockedTime=000001010000Z))(givenName=b*))))", filter);
                 }
             
                 @Test
            @@ -174,7 +186,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(sn=b*)(givenName=b*)))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(sn=b*)(givenName=b*))))", filter);
                 }
             
                 @Test
            @@ -203,7 +215,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(objectCategory=Person)(sAMAccountName=*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(!(pwdAccountLockedTime=000001010000Z)))", filter);
                 }
             
                 @Test
            @@ -216,7 +228,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(sAMAccountName=*bob*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(!(pwdAccountLockedTime=000001010000Z))(sAMAccountName=*bob*)))", filter);
                 }
             
                 @Test
            @@ -281,7 +293,8 @@
                     assertEquals("(&(objectCategory=Role)(cn=admins))", filter);
                 }
             
            -    @Test(expected = NullResultException.class)
            +    @Test
            +    @Ignore("Those nested results no longer return null")
                 public void testSearchInactiveGroups() throws NullResultException {
                     EntityQuery<Group> query = QueryBuilder.queryFor(Group.class, EntityDescriptor.group(GroupType.GROUP)).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching(false)).returningAtMost(10);
            

            Pieter Bruining added a comment - Below the patch for Crowd 4.1.2 Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -1,16 +1,30 @@ package com.atlassian.crowd.directory; + import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper; import com.atlassian.crowd.directory.ldap.credential.EncryptingCredentialEncoder; import com.atlassian.crowd.directory.ldap.credential.LDAPCredentialEncoder; + import com.atlassian.crowd.directory.ldap.mapper.UserContextMapperConfig; + import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper; + import com.atlassian.crowd.directory.ldap.mapper.attribute.user.UserActiveMapper; import com.atlassian.crowd.model.user.User; import com.atlassian.crowd.password.factory.PasswordEncoderFactory; import com.atlassian.crowd.search.ldap.LDAPQueryTranslater; import com.atlassian.crowd.util.InstanceFactory; import com.atlassian.event.api.EventPublisher; + import com.google.common.collect.ImmutableList; + import com.google.common.collect.ImmutableList.Builder; import javax.naming.directory.Attributes; + import java.util.HashMap; + import java.util.List; + import java.util.Map; public class OpenLDAP extends RFC4519Directory { + + private static final String ATTRIBUTE_LOCKOUT_KEY = "pwdAccountLockedTime" ; + /** @see <a href= "http: //linux.die.net/man/5/slapo-ppolicy" >man for slapo-ppolicy</a> */ + private static final String ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED = "000001010000Z" ; + private final PasswordEncoderFactory passwordEncoderFactory; public OpenLDAP(LDAPQueryTranslater ldapQueryTranslater, EventPublisher eventPublisher, InstanceFactory instanceFactory, @@ -25,7 +39,7 @@ @Override public String getDescriptiveName() { - return OpenLDAP.getStaticDirectoryType(); + return OpenLDAP.getStaticDirectoryType() + " (extended with support for sync of active flag, CWD-2762)" ; } @Override @@ -43,4 +57,31 @@ protected void getNewUserDirectorySpecificAttributes( final User user, final Attributes attributes) { addDefaultSnToUserAttributes(attributes, user.getName()); } + + + /** + * As long as the LDAP attributes and values are not configurable, we just add those as hard-coded attributes in here, that they are available within {@link com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper} + * The LDAP attribute keys and values are taken from the de-facto standard "Password Policy for LDAP Directories" + * @see <a href= "http: //tools.ietf.org/html/draft-behera-ldap-password-policy-10" >Password Policy for LDAP Directories</a> + * @see <a href= "http: //linux.die.net/man/5/slapo-ppolicy" >man for slapo-ppolicy</a> + */ + @Override + public void setAttributes(Map< String , String > attributes) { + Map< String , String > extendedAttributes = new HashMap< String , String >(attributes); + + extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_ATTRIBUTE_KEY, ATTRIBUTE_LOCKOUT_KEY); + extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_VALUE, ""); + extendedAttributes.put(LDAPPropertiesMapper.USER_INACTIVE_VALUE, ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED); + + super .setAttributes(extendedAttributes); + } + + @Override + protected List<AttributeMapper> getCustomUserAttributeMappers(UserContextMapperConfig config) { + + Builder<AttributeMapper> builder = ImmutableList.<AttributeMapper>builder(); + builder.addAll( super .getCustomUserAttributeMappers(config)); + builder.add( new UserActiveMapper(ldapPropertiesMapper.getUserActiveAttribute())); + return builder.build(); + } } Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -870,6 +870,10 @@ try { SearchControls searchControls = getSearchControls(contextMapper, SearchControls.OBJECT_SCOPE); + if (contextMapper instanceof ContextMapperWithRequiredAttributes<?>) { + logger.debug( "Return required attributes {} for findEntityById" , (((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes())); + searchControls.setReturningAttributes(toArray(((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes())); + } searchControls.setTimeLimit(ldapPropertiesMapper.getSearchTimeLimit()); entities = ldapTemplate.search(asLdapName(dn, "DN: " + dn, entityClass), filter, searchControls, contextMapper); } catch (NameNotFoundException e) { @@ -971,6 +975,30 @@ modificationItems.add(displayNameMod); } + // support enabling/disabling users + if (supportsInactiveAccounts()) { + ModificationItem activeModItem = null ; + if (!currentUser.isActive() && userTemplate.isActive()) { + // try to activate user in case it is currently inactive + String newValue = ldapPropertiesMapper.getUserActiveValue(); + if (StringUtils.isEmpty(newValue)) { + // rather than modifying an attribute we completely remove it + activeModItem = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute(ldapPropertiesMapper.getUserActiveAttribute())); + } else { + // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value + activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed" , newValue); + } + } else if (currentUser.isActive() && !userTemplate.isActive()) { + // try to deactivate user in case it is currently active + String newValue = ldapPropertiesMapper.getUserInactiveValue(); + // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value + activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed" , newValue); + } + if (activeModItem != null ) + { + modificationItems.add(activeModItem); + } + } return modificationItems; } @@ -1545,7 +1573,7 @@ */ @Override public boolean supportsInactiveAccounts() { - return false ; + return StringUtils.isNotBlank(ldapPropertiesMapper.getUserActiveAttribute()); } @Override Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -176,6 +176,21 @@ */ String USER_PASSWORD_KEY = "ldap.user.password" ; + /** + * Attribute key for the LDAP principal active attribute. + */ + String USER_ACTIVE_ATTRIBUTE_KEY = "ldap.user.active.attribute" ; + + /** + * Attribute key for the LDAP value which stands for active users. In contrast to {@link #USER_INACTIVE_VALUE} this is only considered for writing to the LDAP, not for evaluation! + */ + String USER_ACTIVE_VALUE = "ldap.user.active" ; + + /** + * Attribute key for the LDAP value which stands for inactive users. This is used for checking whether an account is active and also for persisting that information within the LDAP. + */ + String USER_INACTIVE_VALUE = "ldap.user.inactive" ; + /** * Attribute key for the LDAP paged results attribute. */ @@ -364,6 +379,22 @@ String getUserEncryptionMethod(); + /** + * @ return the LDAP attribute in which the users active state is stored. Derived from {@link #USER_ACTIVE_ATTRIBUTE_KEY}. + */ + String getUserActiveAttribute(); + + /** + * @ return the LDAP value which stands for an active user (should only be used for turning a user from inactive to active, i.e. for writing to LDAP, but not for evaluating the state). Derived from {@link #USER_ACTIVE_VALUE}} + */ + String getUserActiveValue(); + + /** + * @ return the LDAP value which stands for an inactive user (should only be used for turning a user from active to inactive, i.e. for writing to LDAP, as well as for evaluating the current state). Derived from {@link #USER_INACTIVE_VALUE}} + */ + String getUserInactiveValue(); + + boolean isPagedResultsControl(); int getPagedResultsSize(); @@ -415,7 +446,7 @@ * compatible method for standardising DNs when mapping object DNs and * and memberDNs (value = <code> false </code>); or if we can use a more * efficient but relaxed form of standardisation (value = <code> true </code>). - * + * <p> * See <code>DNStandardiser</code> for more information. * * @ return <code> false </code> if proper standardisation is required. Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -419,6 +419,22 @@ return getAttribute(LDAP_USER_ENCRYPTION_METHOD); } + @Override + public String getUserActiveAttribute() + { + return getAttribute(USER_ACTIVE_ATTRIBUTE_KEY); + } + + @Override + public String getUserActiveValue() { + return getAttribute(USER_ACTIVE_VALUE); + } + + @Override + public String getUserInactiveValue() { + return getAttribute(USER_INACTIVE_VALUE); + } + /** * Checks if the configuration of the LDAP directory server uses paged results. * Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java (revision 840555138e78f913be9c598bf612e68009847b62) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -0,0 +1,38 @@ + package com.atlassian.crowd.directory.ldap.mapper.attribute.user; + + import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper; + import org.springframework.ldap.core.DirContextAdapter; + + import java.util.Collections; + import java.util.Set; + + public class UserActiveMapper implements AttributeMapper { + + private final String userActiveAttribute; + + public UserActiveMapper( String userActiveAttribute) { + this .userActiveAttribute = userActiveAttribute; + } + + @Override + public String getKey() { + return userActiveAttribute; + } + + @Override + public Set< String > getValues(DirContextAdapter ctx) throws Exception { + // this must never return null + String userActiveValue = ctx.getStringAttribute(getKey()); + if (userActiveValue != null ) { + return Collections.singleton(userActiveValue); + } else { + return Collections.emptySet(); + } + } + + @Override + public Set< String > getRequiredLdapAttributes() { + return Collections.singleton(getKey()); + } + +} Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -68,6 +68,13 @@ // full name putValueInAttributes(populatedUser.getDisplayName(), ldapPropertiesMapper.getUserDisplayNameAttribute(), directoryAttributes); + // active is the default case for LDAP, we only need to write something to the LDAP in case the user is not active + if (!populatedUser.isActive()) { + putValueInAttributes(ldapPropertiesMapper.getUserInactiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + } else { + putValueInAttributes(ldapPropertiesMapper.getUserActiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + } + // TODO: currently we don't support arbitrary attributes / iconLocation / active return directoryAttributes; @@ -129,6 +136,22 @@ } protected boolean getUserActiveFromAttribute( final Attributes directoryAttributes) { + String userActive = DirectoryAttributeRetriever.getValueFromAttributes(ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + if (userActive == null ) { + if (logger.isDebugEnabled()) { + logger.debug( "User active attribute '{}' on user '{}' is not set or could not be evaluated, assuming user is active" , ldapPropertiesMapper.getUserActiveAttribute(), getUsernameFromAttributes(directoryAttributes)); + } + return true ; + } + + if (logger.isDebugEnabled()) { + logger.debug( "User active attribute '{}' is set to '{}' on user {}" , ldapPropertiesMapper.getUserActiveAttribute(), userActive, getUsernameFromAttributes(directoryAttributes)); + } + // is user inactive? + if (userActive.equals(ldapPropertiesMapper.getUserInactiveValue())) { + logger.debug( "User is inactive" ); + return false ; + } return true ; } Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -5,6 +5,7 @@ import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper; import com.atlassian.crowd.embedded.api.SearchRestriction; + import com.atlassian.crowd.search.Entity; import com.atlassian.crowd.search.EntityDescriptor; import com.atlassian.crowd.search.query.entity.EntityQuery; import com.atlassian.crowd.search.query.entity.restriction.BooleanRestriction; @@ -13,12 +14,13 @@ import com.atlassian.crowd.search.query.entity.restriction.PropertyRestriction; import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys; import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys; - + import org.apache.commons.lang3.StringUtils; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.BinaryLogicalFilter; import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.filter.Filter; import org.springframework.ldap.filter.LikeFilter; + import org.springframework.ldap.filter.NotFilter; import org.springframework.ldap.filter.NotPresentFilter; import org.springframework.ldap.filter.OrFilter; import org.springframework.ldap.filter.PresentFilter; @@ -127,16 +129,35 @@ protected Filter booleanTermRestrictionAsFilter( final EntityDescriptor entityDescriptor, final PropertyRestriction< Boolean > termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) { // if boolean term restrictions are for anything other than the group/user active flag, then throw exception - if (!termRestriction.getProperty().equals(GroupTermKeys.ACTIVE) && !termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) { - throw new IllegalArgumentException( " Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported" ); - } else { + if (entityDescriptor.getEntityType() == Entity.USER && termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) { + if (StringUtils.isEmpty(ldapPropertiesMapper.getUserActiveAttribute())) { + if (termRestriction.getValue()) { + // groups are always active = true , so no need to add a restriction + return new EverythingResult(); + } else { + // no group is ever active = false , so no results + return new NothingResult(); + } + } else { + if (termRestriction.getValue()) { + // querying for enabled users + return new NotFilter( new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue())); + } else { + // querying for disabled users + return new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue()); + } + } + } else if (entityDescriptor.getEntityType() == Entity.GROUP && termRestriction.getProperty().equals(GroupTermKeys.ACTIVE)) { if (termRestriction.getValue()) { - // everything is active = true , so no need to add a restriction + // groups are always active = true , so no need to add a restriction return new EverythingResult(); } else { - // nothing is active = false , so need to + // no group is ever active = false , so no results return new NothingResult(); } + } else { + // if boolean term restrictions are for anything other than the group/user active flag, then throw exception + throw new IllegalArgumentException( " Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported" ); } } Index: atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties (revision 840555138e78f913be9c598bf612e68009847b62) @@ -24,6 +24,11 @@ ldap.user.group memberOf ldap.user.password userPassword +# active properties (according to http: //tools.ietf.org/html/draft-behera-ldap-password-policy-10) +ldap.user.active.attribute pwdAccountLockedTime +ldap.user.active +ldap.user.inactive 000001010000Z + # generic options ldap.pagedresults false ldap.relaxed.dn.standardisation true Index: atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java (revision 5f6b1f1d5e856e3c405235ed9b905d5dcc94c650) +++ atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java (revision 840555138e78f913be9c598bf612e68009847b62) @@ -13,8 +13,8 @@ import com.atlassian.crowd.search.query.entity.restriction.PropertyImpl; import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys; import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys; - import org.junit.Before; + import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -48,6 +48,11 @@ when(ldapPropertiesMapper.getGroupNameAttribute()).thenReturn( "cn" ); when(ldapPropertiesMapper.getGroupDescriptionAttribute()).thenReturn( "description" ); when(ldapPropertiesMapper.getGroupMemberAttribute()).thenReturn( "member" ); + + // test user activation through LDAP Password Policy (http://tools.ietf.org/html/draft-behera-ldap-password-policy-10) + when(ldapPropertiesMapper.getUserActiveAttribute()).thenReturn( "pwdAccountLockedTime" ); + when(ldapPropertiesMapper.getUserActiveValue()).thenReturn(""); + when(ldapPropertiesMapper.getUserInactiveValue()).thenReturn( "000001010000Z" ); } @Test @@ -104,7 +109,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(givenName=joe*)(sn=joe*)(displayName=joe*)))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(givenName=joe*)(sn=joe*)(displayName=joe*))(!(pwdAccountLockedTime=000001010000Z))))" , filter); } @Test @@ -116,14 +121,16 @@ assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))" , filter); } - @Test(expected = NullResultException.class) + @Test public void testSearchInactiveUsers() throws NullResultException { EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching( false )).returningAtMost(10); - queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + String filter = ldapQuery.encode(); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(pwdAccountLockedTime=000001010000Z))" , filter); } - @Test(expected = NullResultException.class) + @Test public void testSearchInactiveUsersViaNestingReturningNullResult() throws NullResultException { EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.anyOf( Combine.allOf( @@ -136,7 +143,11 @@ ) )).returningAtMost(10); - queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + String filter = ldapQuery.encode(); + //assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(pwdAccountLockedTime=000001010000Z)(givenName=b*))))" , filter); + } @Test @@ -155,7 +166,8 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(objectCategory=Person)(sAMAccountName=*))" , filter); + // assertEquals( "(&(objectCategory=Person)(sAMAccountName=*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(!(pwdAccountLockedTime=000001010000Z))(displayName=b*))(|(!(pwdAccountLockedTime=000001010000Z))(givenName=b*))))" , filter); } @Test @@ -174,7 +186,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(sn=b*)(givenName=b*)))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(sn=b*)(givenName=b*))))" , filter); } @Test @@ -203,7 +215,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(objectCategory=Person)(sAMAccountName=*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(!(pwdAccountLockedTime=000001010000Z)))" , filter); } @Test @@ -216,7 +228,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(sAMAccountName=*bob*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(!(pwdAccountLockedTime=000001010000Z))(sAMAccountName=*bob*)))" , filter); } @Test @@ -281,7 +293,8 @@ assertEquals( "(&(objectCategory=Role)(cn=admins))" , filter); } - @Test(expected = NullResultException.class) + @Test + @Ignore( "Those nested results no longer return null " ) public void testSearchInactiveGroups() throws NullResultException { EntityQuery<Group> query = QueryBuilder.queryFor(Group.class, EntityDescriptor.group(GroupType.GROUP)).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching( false )).returningAtMost(10);

            We have the same problem. We're using OpenLDAP as backend and need to deactivate users. Not being able to disable users while using active users as licensing factor is not really a customer-friendly strategy... 

            Deleted Account (Inactive) added a comment - We have the same problem. We're using OpenLDAP as backend and need to deactivate users. Not being able to disable users while using active users as licensing factor is not really a customer-friendly strategy... 

            jcron added a comment -

            Dear Atlassian Team (I think justin@atlassian.com has leave Atlassian company because he hasn't got any recent activities since 2016) ...

            Please, could you apply patch from konrad.windszus?

            jcron added a comment - Dear Atlassian Team (I think  justin@atlassian.com  has leave Atlassian company because he hasn't got any recent activities since 2016 ) ... Please, could you apply patch from  konrad.windszus ?

            Here is the patch for 3.7.0

            From 15bf93b1016b9bcb1922fa5a18487acbbc2cb9b6 Fri, 15 Nov 2019 13:24:58 +0100
            From: Konrad Windszus <konrad.windszus@netcentric.biz>
            Date: Wed, 29 Jun 2016 17:50:22 +0200
            Subject: [PATCH] INTERNAL-12645, INTERNAL-13490, INTERNAL-565, INTERNAL-20389 sync deactivation with LDAP
            
            
            diff --git a/readme.md b/readme.md
            new file mode 100644
            index 0000000..82e68bc
            --- /dev/null
            +++ b/readme.md
            @@ -0,0 +1,30 @@
            +# Overview #
            +Customization of the LDAP plugin in Crowd to allow to sync activations/deactivations with LDAP
            +[(see CWD-2762)](https://jira.atlassian.com/browse/CWD-2762).
            +
            +# Original source code/compilation #
            +
            +* [Original Crowd Source Code](https://my.atlassian.com/download/source/crowd)
            +* [Further hints on compilation](https://developer.atlassian.com/display/CROWDDEV/Compiling+the+Crowd+Source)
            +
            +crowd-ldap cannot be compiled with JDT but only with Maven
            +
            +The original source code is in branch "master". All other branches contain the changes for a specific version as one commit.
            +To upgrade to a newer version just rebase that commit to master.
            +
            +# Testing locally #
            +
            +## Prerequisites ##
            +Set up plugin sdk as described [here](https://developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project/install-the-atlassian-sdk-on-a-linux-or-mac-system)
            +
            +Make sure you disable your user's `settings.xml` (e.g. by renaming it) to make the global atlassian SDK `settings.xml` take full effect!
            +
            +## Run/Debug Local Instance ##
            +
            +1. Run regular Maven build: `mvn clean install`
            +2. `atlas-run-standalone --product crowd --version 2.11.2 --lib-plugins com.atlassian.crowd:crowd-ldap:2.11.1 --jvmargs "-Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n"` (starts Crowd [locally](http://localhost:4990/crowd) and deploys this plugin in it.).
            +The default `atlas-run` will not work, as this will still run with the default ldap-plugin (below /target/crowd/webapp/WEB-INF/lib)
            +Further starting options described [here](https://developer.atlassian.com/docs/developer-tools/working-with-the-sdk/command-reference/atlas-run)
            +3. Run a local ldap from [Apache Directory Studio](http://directory.apache.org/studio/downloads.html)
            +4. Set up a new directory connector in Crowd via http://localhost:4990/crowd/console/secure/directory/createconnector.action on `ldap://localhost:10389` with bind dn `uid=admin,ou=system` and pw `secret` and base dn `dc=example,dc=com`.
            +5. Connect through a remote debugger with your IDE on port 8000
            diff --git a/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java b/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java
            index a1296c4..1036646 100644
            --- a/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java
            +++ b/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java
            @@ -10,7 +10,22 @@
             
             import javax.naming.directory.Attributes;
             
            +import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper;
            +import com.atlassian.crowd.directory.ldap.mapper.UserContextMapperConfig;
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper;
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.user.UserActiveMapper;
            +import com.google.common.collect.ImmutableList;
            +import com.google.common.collect.ImmutableList.Builder;
            +import java.util.HashMap;
            +import java.util.List;
            +import java.util.Map;
            +
             public class OpenLDAP extends RFC4519Directory {
            +
            +    private static final String ATTRIBUTE_LOCKOUT_KEY = "pwdAccountLockedTime";
            +    /** @see <a href="http://linux.die.net/man/5/slapo-ppolicy">man for slapo-ppolicy</a> */
            +    private static final String ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED = "000001010000Z";
            +    
                 private final PasswordEncoderFactory passwordEncoderFactory;
             
                 public OpenLDAP(LDAPQueryTranslater ldapQueryTranslater, EventPublisher eventPublisher, InstanceFactory instanceFactory,
            @@ -25,7 +40,7 @@
             
                 @Override
                 public String getDescriptiveName() {
            -        return OpenLDAP.getStaticDirectoryType();
            +        return OpenLDAP.getStaticDirectoryType() + " (extended with support for sync of active flag, CWD-2762)";
                 }
             
                 @Override
            @@ -43,4 +58,32 @@
                 protected void getNewUserDirectorySpecificAttributes(final User user, final Attributes attributes) {
                     addDefaultSnToUserAttributes(attributes, user.getName());
                 }
            +
            +    /**
            +     * As long as the LDAP attributes and values are not configurable, we just add those as hard-coded attributes in here, that they are available within {@link com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper}
            +     * The LDAP attribute keys and values are taken from the de-facto standard "Password Policy for LDAP Directories"
            +     * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy-10">Password Policy for LDAP Directories</a>
            +     * @see <a href="http://linux.die.net/man/5/slapo-ppolicy">man for slapo-ppolicy</a> 
            +     */
            +    @Override
            +    public void setAttributes(Map<String, String> attributes) {
            +        Map<String, String> extendedAttributes = new HashMap<>(attributes);
            +
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_ATTRIBUTE_KEY, ATTRIBUTE_LOCKOUT_KEY);
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_VALUE, "");
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_INACTIVE_VALUE, ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED);
            +
            +        super.setAttributes(extendedAttributes);
            +    }
            +
            +    /**
            +     * Combine the custom user attribute mappers with the {@link UserActiveMapper}.
            +     */
            +    @Override
            +    protected List<AttributeMapper> getCustomUserAttributeMappers(UserContextMapperConfig config) {
            +        Builder<AttributeMapper> builder = ImmutableList.<AttributeMapper> builder();
            +        builder.addAll(super.getCustomUserAttributeMappers(config));
            +        builder.add(new UserActiveMapper(ldapPropertiesMapper.getUserActiveAttribute()));
            +        return builder.build();
            +    }
             }
            diff --git a/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java b/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java
            index f4ca104..9cb5914 100644
            --- a/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java
            +++ b/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java
            @@ -870,6 +870,13 @@
             
                         try {
                             SearchControls searchControls = getSearchControls(contextMapper, SearchControls.OBJECT_SCOPE);
            +                // make sure to really return also operational attributes (e.g. ones to determine active state of the user)
            +                if (contextMapper instanceof ContextMapperWithRequiredAttributes<?>) {
            +                    logger.debug("Return required attributes {} for findEntityById",
            +                            (((ContextMapperWithRequiredAttributes<?>) contextMapper).getRequiredLdapAttributes()));
            +                    searchControls.setReturningAttributes(
            +                            toArray(((ContextMapperWithRequiredAttributes<?>) contextMapper).getRequiredLdapAttributes()));
            +                }
                             searchControls.setTimeLimit(ldapPropertiesMapper.getSearchTimeLimit());
                             entities = ldapTemplate.search(asLdapName(dn, "DN: " + dn, entityClass), filter, searchControls, contextMapper);
                         } catch (NameNotFoundException e) {
            @@ -971,6 +978,32 @@
                         modificationItems.add(displayNameMod);
                     }
             
            +        // support enabling/disabling users
            +        if (supportsInactiveAccounts()) {
            +            ModificationItem activeModItem = null;
            +            if (!currentUser.isActive() && userTemplate.isActive()) {
            +                // try to activate user in case it is currently inactive
            +                String newValue = ldapPropertiesMapper.getUserActiveValue();
            +                if (StringUtils.isEmpty(newValue)) {
            +                    // rather than modifying an attribute we completely remove it
            +                    activeModItem = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
            +                            new BasicAttribute(ldapPropertiesMapper.getUserActiveAttribute()));
            +                } else {
            +                    // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old
            +                    // value to something else than new value
            +                    activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed", newValue);
            +                }
            +            } else if (currentUser.isActive() && !userTemplate.isActive()) {
            +                // try to deactivate user in case it is currently active
            +                String newValue = ldapPropertiesMapper.getUserInactiveValue();
            +                // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old
            +                // value to something else than new value
            +                activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed", newValue);
            +            }
            +            if (activeModItem != null) {
            +                modificationItems.add(activeModItem);
            +            }
            +        }
                     return modificationItems;
                 }
             
            @@ -1545,7 +1578,7 @@
                  */
                 @Override
                 public boolean supportsInactiveAccounts() {
            -        return false;
            +        return StringUtils.isNotBlank(ldapPropertiesMapper.getUserActiveAttribute());
                 }
             
                 @Override
            diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java
            index 4668c2f..7c24ab9 100644
            --- a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java
            +++ b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java
            @@ -175,6 +175,21 @@
                  * Attribute key for the LDAP principal password attribute.
                  */
                 String USER_PASSWORD_KEY = "ldap.user.password";
            +    
            +    /**
            +     * Attribute key for the LDAP principal active attribute.
            +     */
            +    String USER_ACTIVE_ATTRIBUTE_KEY = "ldap.user.active.attribute";
            +    
            +    /**
            +     * Attribute key for the LDAP value which stands for active users. In contrast to {@link #USER_INACTIVE_VALUE} this is only considered for writing to the LDAP, not for evaluation!
            +     */
            +    String USER_ACTIVE_VALUE = "ldap.user.active";
            +    
            +    /**
            +     * Attribute key for the LDAP value which stands for inactive users. This is used for checking whether an account is active and also for persisting that information within the LDAP.
            +     */
            +    String USER_INACTIVE_VALUE = "ldap.user.inactive";
             
                 /**
                  * Attribute key for the LDAP paged results attribute.
            @@ -363,6 +378,25 @@
                 String getUserPasswordAttribute();
             
                 String getUserEncryptionMethod();
            +    
            +    /**
            +     * 
            +     * @return the LDAP attribute in which the users active state is stored. Derived from {@link #USER_ACTIVE_ATTRIBUTE_KEY}.
            +     */
            +    String getUserActiveAttribute();
            +    
            +    /**
            +     * 
            +     * @return the LDAP value which stands for an active user (should only be used for turning a user from inactive to active, i.e. for writing to LDAP, but not for evaluating the state). Derived from {@link #USER_ACTIVE_VALUE}}
            +     */
            +    String getUserActiveValue();
            +    
            +    /**
            +     * 
            +     * @return the LDAP value which stands for an inactive user (should only be used for turning a user from active to inactive, i.e. for writing to LDAP, as well as for evaluating the current state). Derived from {@link #USER_INACTIVE_VALUE}}
            +     */
            +    String getUserInactiveValue();
            +    
             
                 boolean isPagedResultsControl();
             
            diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java
            index 7b07ed5..a82799c 100644
            --- a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java
            +++ b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java
            @@ -418,6 +418,22 @@
                 public String getUserEncryptionMethod() {
                     return getAttribute(LDAP_USER_ENCRYPTION_METHOD);
                 }
            +    
            +    @Override
            +    public String getUserActiveAttribute()
            +    {
            +    	return getAttribute(USER_ACTIVE_ATTRIBUTE_KEY);
            +    }
            +    
            +    @Override
            +    public String getUserActiveValue() {
            +    	return getAttribute(USER_ACTIVE_VALUE);
            +    }
            +    
            +    @Override
            +    public String getUserInactiveValue() {
            +    	return getAttribute(USER_INACTIVE_VALUE);
            +    }
             
                 /**
                  * Checks if the configuration of the LDAP directory server uses paged results.
            diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java
            new file mode 100644
            index 0000000..284c0c0
            --- /dev/null
            +++ b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java
            @@ -0,0 +1,42 @@
            +package com.atlassian.crowd.directory.ldap.mapper.attribute.user;
            +
            +import java.util.Collections;
            +import java.util.Set;
            +
            +import org.springframework.ldap.core.DirContextAdapter;
            +
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper;
            +
            +/**
            + * Attribute mapper which takes care of the active state of the LDAP user.
            + */
            +public class UserActiveMapper implements AttributeMapper {
            +
            +    private final String userActiveAttribute;
            +
            +    public UserActiveMapper(String userActiveAttribute) {
            +        this.userActiveAttribute = userActiveAttribute;
            +    }
            +
            +    @Override
            +    public String getKey() {
            +        return userActiveAttribute;
            +    }
            +
            +    @Override
            +    public Set<String> getValues(DirContextAdapter ctx) throws Exception {
            +        // this must never return null
            +        String userActiveValue = ctx.getStringAttribute(getKey());
            +        if (userActiveValue != null) {
            +            return Collections.singleton(userActiveValue);
            +        } else {
            +            return Collections.emptySet();
            +        }
            +    }
            +
            +    @Override
            +    public Set<String> getRequiredLdapAttributes() {
            +        return Collections.singleton(getKey());
            +    }
            +
            +}
            diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java
            index eda1734..68a9a69 100644
            --- a/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java
            +++ b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java
            @@ -67,6 +67,13 @@
             
                     // full name
                     putValueInAttributes(populatedUser.getDisplayName(), ldapPropertiesMapper.getUserDisplayNameAttribute(), directoryAttributes);
            +        
            +        // active is the default case for LDAP, we only need to write something to the LDAP in case the user is not active
            +        if (!populatedUser.isActive()) {
            +            putValueInAttributes(ldapPropertiesMapper.getUserInactiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        } else {
            +            putValueInAttributes(ldapPropertiesMapper.getUserActiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        }
             
                     // TODO: currently we don't support arbitrary attributes / iconLocation / active
             
            @@ -129,6 +136,24 @@
                 }
             
                 protected boolean getUserActiveFromAttribute(final Attributes directoryAttributes) {
            +        String userActive = DirectoryAttributeRetriever.getValueFromAttributes(ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        if (userActive == null) {
            +            if (logger.isDebugEnabled()) {
            +                logger.debug("User active attribute '{}' on user '{}' is not set or could not be evaluated, assuming user is active",
            +                        ldapPropertiesMapper.getUserActiveAttribute(), getUsernameFromAttributes(directoryAttributes));
            +            }
            +            return true;
            +        }
            +
            +        if (logger.isDebugEnabled()) {
            +            logger.debug("User active attribute '{}' is set to '{}' on user {}", ldapPropertiesMapper.getUserActiveAttribute(), userActive,
            +                    getUsernameFromAttributes(directoryAttributes));
            +        }
            +        // is user inactive?
            +        if (userActive.equals(ldapPropertiesMapper.getUserInactiveValue())) {
            +            logger.debug("User is inactive");
            +            return false;
            +        }
                     return true;
                 }
             
            diff --git a/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java b/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java
            index 243f2ab..bee3f9e 100644
            --- a/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java
            +++ b/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java
            @@ -5,6 +5,7 @@
             
             import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper;
             import com.atlassian.crowd.embedded.api.SearchRestriction;
            +import com.atlassian.crowd.search.Entity;
             import com.atlassian.crowd.search.EntityDescriptor;
             import com.atlassian.crowd.search.query.entity.EntityQuery;
             import com.atlassian.crowd.search.query.entity.restriction.BooleanRestriction;
            @@ -14,11 +15,13 @@
             import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys;
             import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys;
             
            +import org.apache.commons.lang3.StringUtils;
             import org.springframework.ldap.filter.AndFilter;
             import org.springframework.ldap.filter.BinaryLogicalFilter;
             import org.springframework.ldap.filter.EqualsFilter;
             import org.springframework.ldap.filter.Filter;
             import org.springframework.ldap.filter.LikeFilter;
            +import org.springframework.ldap.filter.NotFilter;
             import org.springframework.ldap.filter.NotPresentFilter;
             import org.springframework.ldap.filter.OrFilter;
             import org.springframework.ldap.filter.PresentFilter;
            @@ -125,18 +128,39 @@
                 }
             
             
            -    protected Filter booleanTermRestrictionAsFilter(final EntityDescriptor entityDescriptor, final PropertyRestriction<Boolean> termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) {
            -        // if boolean term restrictions are for anything other than the group/user active flag, then throw exception
            -        if (!termRestriction.getProperty().equals(GroupTermKeys.ACTIVE) && !termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) {
            -            throw new IllegalArgumentException("Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported");
            -        } else {
            +    protected Filter booleanTermRestrictionAsFilter(final EntityDescriptor entityDescriptor,
            +            final PropertyRestriction<Boolean> termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) {
            +        if (entityDescriptor.getEntityType() == Entity.USER && termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) {
            +            if (StringUtils.isEmpty(ldapPropertiesMapper.getUserActiveAttribute())) {
            +                if (termRestriction.getValue()) {
            +                    // don't consider active because user activeAttribute is not set (i.e. return all users)
            +                    return new EverythingResult();
            +                } else {
            +                    // don't consider disabled because user activeAttribute is not set (i.e. return no users)
            +                    return new NothingResult();
            +                }
            +            } else {
            +                if (termRestriction.getValue()) {
            +                    // querying for enabled users
            +                    return new NotFilter(
            +                            new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue()));
            +                } else {
            +                    // querying for disabled users
            +                    return new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue());
            +                }
            +            }
            +        } else if (entityDescriptor.getEntityType() == Entity.GROUP && termRestriction.getProperty().equals(GroupTermKeys.ACTIVE)) {
                         if (termRestriction.getValue()) {
            -                // everything is active = true, so no need to add a restriction
            +                // groups are always active = true, so no need to add a restriction
                             return new EverythingResult();
                         } else {
            -                // nothing is active = false, so need to
            +                // no group is ever active = false, so no results
                             return new NothingResult();
                         }
            +        } else {
            +            // if boolean term restrictions are for anything other than the group/user active flag, then throw exception
            +            throw new IllegalArgumentException(
            +                    "Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported");
                     }
                 }
             
            diff --git a/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties b/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties
            index 36cb5db..9e53bec 100644
            --- a/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties
            +++ b/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties
            @@ -24,6 +24,11 @@
             ldap.user.group         memberOf
             ldap.user.password      userPassword
             
            +# active properties (according to http://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
            +ldap.user.active.attribute	pwdAccountLockedTime
            +ldap.user.active		
            +ldap.user.inactive		000001010000Z
            +
             # generic options
             ldap.pagedresults               false
             ldap.relaxed.dn.standardisation true
            diff --git a/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java b/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java
            index 8314fa2..2c48d4b 100644
            --- a/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java
            +++ b/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java
            @@ -1,5 +1,13 @@
             package com.atlassian.crowd.search.ldap;
             
            +import static org.junit.Assert.assertEquals;
            +import static org.mockito.Mockito.mock;
            +import static org.mockito.Mockito.when;
            +
            +import org.junit.Before;
            +import org.junit.Ignore;
            +import org.junit.Test;
            +
             import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper;
             import com.atlassian.crowd.model.group.Group;
             import com.atlassian.crowd.model.group.GroupType;
            @@ -14,13 +22,6 @@
             import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys;
             import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys;
             
            -import org.junit.Before;
            -import org.junit.Test;
            -
            -import static org.junit.Assert.assertEquals;
            -import static org.mockito.Mockito.mock;
            -import static org.mockito.Mockito.when;
            -
             public class LDAPQueryTranslaterImplTest {
                 private static final Property<String> CUSTOM_STRING_PROPERTY = new PropertyImpl<String>("myproperty", String.class);
             
            @@ -48,6 +49,11 @@
                     when(ldapPropertiesMapper.getGroupNameAttribute()).thenReturn("cn");
                     when(ldapPropertiesMapper.getGroupDescriptionAttribute()).thenReturn("description");
                     when(ldapPropertiesMapper.getGroupMemberAttribute()).thenReturn("member");
            +        
            +        // test user activation through LDAP Password Policy (http://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
            +        when(ldapPropertiesMapper.getUserActiveAttribute()).thenReturn("pwdAccountLockedTime");
            +        when(ldapPropertiesMapper.getUserActiveValue()).thenReturn("");
            +        when(ldapPropertiesMapper.getUserInactiveValue()).thenReturn("000001010000Z");
                 }
             
                 @Test
            @@ -104,7 +110,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(givenName=joe*)(sn=joe*)(displayName=joe*)))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(givenName=joe*)(sn=joe*)(displayName=joe*))(!(pwdAccountLockedTime=000001010000Z))))", filter);
                 }
             
                 @Test
            @@ -116,14 +122,16 @@
                     assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))", filter);
                 }
             
            -    @Test(expected = NullResultException.class)
            +    @Test
                 public void testSearchInactiveUsers() throws NullResultException {
                     EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching(false)).returningAtMost(10);
             
            -        queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        String filter = ldapQuery.encode();
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(pwdAccountLockedTime=000001010000Z))", filter);
                 }
            -
            -    @Test(expected = NullResultException.class)
            +    @Test
            +    @Ignore("Those nested results no longer return null")
                 public void testSearchInactiveUsersViaNestingReturningNullResult() throws NullResultException {
                     EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.anyOf(
                             Combine.allOf(
            @@ -136,10 +144,14 @@
                             )
                     )).returningAtMost(10);
             
            -        queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        String filter = ldapQuery.encode();
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))", filter);
            +        
                 }
             
                 @Test
            +    @Ignore("This test is no longer applicable since the nested conditions don't return everything")
                 public void testSearchActiveUsersViaNestingReturningEverything() throws NullResultException {
                     EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.allOf(
                             Combine.anyOf(
            @@ -174,7 +186,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(sn=b*)(givenName=b*)))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(sn=b*)(givenName=b*))))", filter);
                 }
             
                 @Test
            @@ -203,7 +215,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(objectCategory=Person)(sAMAccountName=*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(!(pwdAccountLockedTime=000001010000Z)))", filter);
                 }
             
                 @Test
            @@ -216,7 +228,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
             
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(sAMAccountName=*bob*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(!(pwdAccountLockedTime=000001010000Z))(sAMAccountName=*bob*)))", filter);
                 }
             
                 @Test
            

            Konrad Windszus added a comment - Here is the patch for 3.7.0 From 15bf93b1016b9bcb1922fa5a18487acbbc2cb9b6 Fri, 15 Nov 2019 13:24:58 +0100 From: Konrad Windszus <konrad.windszus@netcentric.biz> Date: Wed, 29 Jun 2016 17:50:22 +0200 Subject: [PATCH] INTERNAL-12645, INTERNAL-13490, INTERNAL-565, INTERNAL-20389 sync deactivation with LDAP diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..82e68bc --- /dev/ null +++ b/readme.md @@ -0,0 +1,30 @@ +# Overview # +Customization of the LDAP plugin in Crowd to allow to sync activations/deactivations with LDAP +[(see CWD-2762)](https: //jira.atlassian.com/browse/CWD-2762). + +# Original source code/compilation # + +* [Original Crowd Source Code](https: //my.atlassian.com/download/source/crowd) +* [Further hints on compilation](https: //developer.atlassian.com/display/CROWDDEV/Compiling+the+Crowd+Source) + +crowd-ldap cannot be compiled with JDT but only with Maven + +The original source code is in branch "master" . All other branches contain the changes for a specific version as one commit. +To upgrade to a newer version just rebase that commit to master. + +# Testing locally # + +## Prerequisites ## +Set up plugin sdk as described [here](https: //developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project/install-the-atlassian-sdk-on-a-linux-or-mac-system) + +Make sure you disable your user's `settings.xml` (e.g. by renaming it) to make the global atlassian SDK `settings.xml` take full effect! + +## Run/Debug Local Instance ## + +1. Run regular Maven build: `mvn clean install` +2. `atlas-run-standalone --product crowd --version 2.11.2 --lib-plugins com.atlassian.crowd:crowd-ldap:2.11.1 --jvmargs "-Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n" ` (starts Crowd [locally](http: //localhost:4990/crowd) and deploys this plugin in it.). +The default `atlas-run` will not work, as this will still run with the default ldap-plugin (below /target/crowd/webapp/WEB-INF/lib) +Further starting options described [here](https: //developer.atlassian.com/docs/developer-tools/working-with-the-sdk/command-reference/atlas-run) +3. Run a local ldap from [Apache Directory Studio](http: //directory.apache.org/studio/downloads.html) +4. Set up a new directory connector in Crowd via http: //localhost:4990/crowd/console/secure/directory/createconnector.action on `ldap://localhost:10389` with bind dn `uid=admin,ou=system` and pw `secret` and base dn `dc=example,dc=com`. +5. Connect through a remote debugger with your IDE on port 8000 diff --git a/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java b/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java index a1296c4..1036646 100644 --- a/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java +++ b/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java @@ -10,7 +10,22 @@ import javax.naming.directory.Attributes; + import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper; + import com.atlassian.crowd.directory.ldap.mapper.UserContextMapperConfig; + import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper; + import com.atlassian.crowd.directory.ldap.mapper.attribute.user.UserActiveMapper; + import com.google.common.collect.ImmutableList; + import com.google.common.collect.ImmutableList.Builder; + import java.util.HashMap; + import java.util.List; + import java.util.Map; + public class OpenLDAP extends RFC4519Directory { + + private static final String ATTRIBUTE_LOCKOUT_KEY = "pwdAccountLockedTime" ; + /** @see <a href= "http: //linux.die.net/man/5/slapo-ppolicy" >man for slapo-ppolicy</a> */ + private static final String ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED = "000001010000Z" ; + private final PasswordEncoderFactory passwordEncoderFactory; public OpenLDAP(LDAPQueryTranslater ldapQueryTranslater, EventPublisher eventPublisher, InstanceFactory instanceFactory, @@ -25,7 +40,7 @@ @Override public String getDescriptiveName() { - return OpenLDAP.getStaticDirectoryType(); + return OpenLDAP.getStaticDirectoryType() + " (extended with support for sync of active flag, CWD-2762)" ; } @Override @@ -43,4 +58,32 @@ protected void getNewUserDirectorySpecificAttributes( final User user, final Attributes attributes) { addDefaultSnToUserAttributes(attributes, user.getName()); } + + /** + * As long as the LDAP attributes and values are not configurable, we just add those as hard-coded attributes in here, that they are available within {@link com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper} + * The LDAP attribute keys and values are taken from the de-facto standard "Password Policy for LDAP Directories" + * @see <a href= "http: //tools.ietf.org/html/draft-behera-ldap-password-policy-10" >Password Policy for LDAP Directories</a> + * @see <a href= "http: //linux.die.net/man/5/slapo-ppolicy" >man for slapo-ppolicy</a> + */ + @Override + public void setAttributes(Map< String , String > attributes) { + Map< String , String > extendedAttributes = new HashMap<>(attributes); + + extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_ATTRIBUTE_KEY, ATTRIBUTE_LOCKOUT_KEY); + extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_VALUE, ""); + extendedAttributes.put(LDAPPropertiesMapper.USER_INACTIVE_VALUE, ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED); + + super .setAttributes(extendedAttributes); + } + + /** + * Combine the custom user attribute mappers with the {@link UserActiveMapper}. + */ + @Override + protected List<AttributeMapper> getCustomUserAttributeMappers(UserContextMapperConfig config) { + Builder<AttributeMapper> builder = ImmutableList.<AttributeMapper> builder(); + builder.addAll( super .getCustomUserAttributeMappers(config)); + builder.add( new UserActiveMapper(ldapPropertiesMapper.getUserActiveAttribute())); + return builder.build(); + } } diff --git a/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java b/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java index f4ca104..9cb5914 100644 --- a/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java +++ b/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java @@ -870,6 +870,13 @@ try { SearchControls searchControls = getSearchControls(contextMapper, SearchControls.OBJECT_SCOPE); + // make sure to really return also operational attributes (e.g. ones to determine active state of the user) + if (contextMapper instanceof ContextMapperWithRequiredAttributes<?>) { + logger.debug( "Return required attributes {} for findEntityById" , + (((ContextMapperWithRequiredAttributes<?>) contextMapper).getRequiredLdapAttributes())); + searchControls.setReturningAttributes( + toArray(((ContextMapperWithRequiredAttributes<?>) contextMapper).getRequiredLdapAttributes())); + } searchControls.setTimeLimit(ldapPropertiesMapper.getSearchTimeLimit()); entities = ldapTemplate.search(asLdapName(dn, "DN: " + dn, entityClass), filter, searchControls, contextMapper); } catch (NameNotFoundException e) { @@ -971,6 +978,32 @@ modificationItems.add(displayNameMod); } + // support enabling/disabling users + if (supportsInactiveAccounts()) { + ModificationItem activeModItem = null ; + if (!currentUser.isActive() && userTemplate.isActive()) { + // try to activate user in case it is currently inactive + String newValue = ldapPropertiesMapper.getUserActiveValue(); + if (StringUtils.isEmpty(newValue)) { + // rather than modifying an attribute we completely remove it + activeModItem = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, + new BasicAttribute(ldapPropertiesMapper.getUserActiveAttribute())); + } else { + // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old + // value to something else than new value + activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed" , newValue); + } + } else if (currentUser.isActive() && !userTemplate.isActive()) { + // try to deactivate user in case it is currently active + String newValue = ldapPropertiesMapper.getUserInactiveValue(); + // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old + // value to something else than new value + activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed" , newValue); + } + if (activeModItem != null ) { + modificationItems.add(activeModItem); + } + } return modificationItems; } @@ -1545,7 +1578,7 @@ */ @Override public boolean supportsInactiveAccounts() { - return false ; + return StringUtils.isNotBlank(ldapPropertiesMapper.getUserActiveAttribute()); } @Override diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java index 4668c2f..7c24ab9 100644 --- a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java +++ b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java @@ -175,6 +175,21 @@ * Attribute key for the LDAP principal password attribute. */ String USER_PASSWORD_KEY = "ldap.user.password" ; + + /** + * Attribute key for the LDAP principal active attribute. + */ + String USER_ACTIVE_ATTRIBUTE_KEY = "ldap.user.active.attribute" ; + + /** + * Attribute key for the LDAP value which stands for active users. In contrast to {@link #USER_INACTIVE_VALUE} this is only considered for writing to the LDAP, not for evaluation! + */ + String USER_ACTIVE_VALUE = "ldap.user.active" ; + + /** + * Attribute key for the LDAP value which stands for inactive users. This is used for checking whether an account is active and also for persisting that information within the LDAP. + */ + String USER_INACTIVE_VALUE = "ldap.user.inactive" ; /** * Attribute key for the LDAP paged results attribute. @@ -363,6 +378,25 @@ String getUserPasswordAttribute(); String getUserEncryptionMethod(); + + /** + * + * @ return the LDAP attribute in which the users active state is stored. Derived from {@link #USER_ACTIVE_ATTRIBUTE_KEY}. + */ + String getUserActiveAttribute(); + + /** + * + * @ return the LDAP value which stands for an active user (should only be used for turning a user from inactive to active, i.e. for writing to LDAP, but not for evaluating the state). Derived from {@link #USER_ACTIVE_VALUE}} + */ + String getUserActiveValue(); + + /** + * + * @ return the LDAP value which stands for an inactive user (should only be used for turning a user from active to inactive, i.e. for writing to LDAP, as well as for evaluating the current state). Derived from {@link #USER_INACTIVE_VALUE}} + */ + String getUserInactiveValue(); + boolean isPagedResultsControl(); diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java index 7b07ed5..a82799c 100644 --- a/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java +++ b/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java @@ -418,6 +418,22 @@ public String getUserEncryptionMethod() { return getAttribute(LDAP_USER_ENCRYPTION_METHOD); } + + @Override + public String getUserActiveAttribute() + { + return getAttribute(USER_ACTIVE_ATTRIBUTE_KEY); + } + + @Override + public String getUserActiveValue() { + return getAttribute(USER_ACTIVE_VALUE); + } + + @Override + public String getUserInactiveValue() { + return getAttribute(USER_INACTIVE_VALUE); + } /** * Checks if the configuration of the LDAP directory server uses paged results. diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java new file mode 100644 index 0000000..284c0c0 --- /dev/ null +++ b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java @@ -0,0 +1,42 @@ + package com.atlassian.crowd.directory.ldap.mapper.attribute.user; + + import java.util.Collections; + import java.util.Set; + + import org.springframework.ldap.core.DirContextAdapter; + + import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper; + +/** + * Attribute mapper which takes care of the active state of the LDAP user. + */ + public class UserActiveMapper implements AttributeMapper { + + private final String userActiveAttribute; + + public UserActiveMapper( String userActiveAttribute) { + this .userActiveAttribute = userActiveAttribute; + } + + @Override + public String getKey() { + return userActiveAttribute; + } + + @Override + public Set< String > getValues(DirContextAdapter ctx) throws Exception { + // this must never return null + String userActiveValue = ctx.getStringAttribute(getKey()); + if (userActiveValue != null ) { + return Collections.singleton(userActiveValue); + } else { + return Collections.emptySet(); + } + } + + @Override + public Set< String > getRequiredLdapAttributes() { + return Collections.singleton(getKey()); + } + +} diff --git a/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java index eda1734..68a9a69 100644 --- a/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java +++ b/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java @@ -67,6 +67,13 @@ // full name putValueInAttributes(populatedUser.getDisplayName(), ldapPropertiesMapper.getUserDisplayNameAttribute(), directoryAttributes); + + // active is the default case for LDAP, we only need to write something to the LDAP in case the user is not active + if (!populatedUser.isActive()) { + putValueInAttributes(ldapPropertiesMapper.getUserInactiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + } else { + putValueInAttributes(ldapPropertiesMapper.getUserActiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + } // TODO: currently we don't support arbitrary attributes / iconLocation / active @@ -129,6 +136,24 @@ } protected boolean getUserActiveFromAttribute( final Attributes directoryAttributes) { + String userActive = DirectoryAttributeRetriever.getValueFromAttributes(ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + if (userActive == null ) { + if (logger.isDebugEnabled()) { + logger.debug( "User active attribute '{}' on user '{}' is not set or could not be evaluated, assuming user is active" , + ldapPropertiesMapper.getUserActiveAttribute(), getUsernameFromAttributes(directoryAttributes)); + } + return true ; + } + + if (logger.isDebugEnabled()) { + logger.debug( "User active attribute '{}' is set to '{}' on user {}" , ldapPropertiesMapper.getUserActiveAttribute(), userActive, + getUsernameFromAttributes(directoryAttributes)); + } + // is user inactive? + if (userActive.equals(ldapPropertiesMapper.getUserInactiveValue())) { + logger.debug( "User is inactive" ); + return false ; + } return true ; } diff --git a/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java b/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java index 243f2ab..bee3f9e 100644 --- a/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java +++ b/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java @@ -5,6 +5,7 @@ import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper; import com.atlassian.crowd.embedded.api.SearchRestriction; + import com.atlassian.crowd.search.Entity; import com.atlassian.crowd.search.EntityDescriptor; import com.atlassian.crowd.search.query.entity.EntityQuery; import com.atlassian.crowd.search.query.entity.restriction.BooleanRestriction; @@ -14,11 +15,13 @@ import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys; import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys; + import org.apache.commons.lang3.StringUtils; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.BinaryLogicalFilter; import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.filter.Filter; import org.springframework.ldap.filter.LikeFilter; + import org.springframework.ldap.filter.NotFilter; import org.springframework.ldap.filter.NotPresentFilter; import org.springframework.ldap.filter.OrFilter; import org.springframework.ldap.filter.PresentFilter; @@ -125,18 +128,39 @@ } - protected Filter booleanTermRestrictionAsFilter( final EntityDescriptor entityDescriptor, final PropertyRestriction< Boolean > termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) { - // if boolean term restrictions are for anything other than the group/user active flag, then throw exception - if (!termRestriction.getProperty().equals(GroupTermKeys.ACTIVE) && !termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) { - throw new IllegalArgumentException( " Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported" ); - } else { + protected Filter booleanTermRestrictionAsFilter( final EntityDescriptor entityDescriptor, + final PropertyRestriction< Boolean > termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) { + if (entityDescriptor.getEntityType() == Entity.USER && termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) { + if (StringUtils.isEmpty(ldapPropertiesMapper.getUserActiveAttribute())) { + if (termRestriction.getValue()) { + // don't consider active because user activeAttribute is not set (i.e. return all users) + return new EverythingResult(); + } else { + // don't consider disabled because user activeAttribute is not set (i.e. return no users) + return new NothingResult(); + } + } else { + if (termRestriction.getValue()) { + // querying for enabled users + return new NotFilter( + new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue())); + } else { + // querying for disabled users + return new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue()); + } + } + } else if (entityDescriptor.getEntityType() == Entity.GROUP && termRestriction.getProperty().equals(GroupTermKeys.ACTIVE)) { if (termRestriction.getValue()) { - // everything is active = true , so no need to add a restriction + // groups are always active = true , so no need to add a restriction return new EverythingResult(); } else { - // nothing is active = false , so need to + // no group is ever active = false , so no results return new NothingResult(); } + } else { + // if boolean term restrictions are for anything other than the group/user active flag, then throw exception + throw new IllegalArgumentException( + " Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported" ); } } diff --git a/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties b/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties index 36cb5db..9e53bec 100644 --- a/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties +++ b/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties @@ -24,6 +24,11 @@ ldap.user.group memberOf ldap.user.password userPassword +# active properties (according to http: //tools.ietf.org/html/draft-behera-ldap-password-policy-10) +ldap.user.active.attribute pwdAccountLockedTime +ldap.user.active +ldap.user.inactive 000001010000Z + # generic options ldap.pagedresults false ldap.relaxed.dn.standardisation true diff --git a/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java b/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java index 8314fa2..2c48d4b 100644 --- a/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java +++ b/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java @@ -1,5 +1,13 @@ package com.atlassian.crowd.search.ldap; + import static org.junit.Assert.assertEquals; + import static org.mockito.Mockito.mock; + import static org.mockito.Mockito.when; + + import org.junit.Before; + import org.junit.Ignore; + import org.junit.Test; + import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper; import com.atlassian.crowd.model.group.Group; import com.atlassian.crowd.model.group.GroupType; @@ -14,13 +22,6 @@ import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys; import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys; - import org.junit.Before; - import org.junit.Test; - - import static org.junit.Assert.assertEquals; - import static org.mockito.Mockito.mock; - import static org.mockito.Mockito.when; - public class LDAPQueryTranslaterImplTest { private static final Property< String > CUSTOM_STRING_PROPERTY = new PropertyImpl< String >( "myproperty" , String .class); @@ -48,6 +49,11 @@ when(ldapPropertiesMapper.getGroupNameAttribute()).thenReturn( "cn" ); when(ldapPropertiesMapper.getGroupDescriptionAttribute()).thenReturn( "description" ); when(ldapPropertiesMapper.getGroupMemberAttribute()).thenReturn( "member" ); + + // test user activation through LDAP Password Policy (http://tools.ietf.org/html/draft-behera-ldap-password-policy-10) + when(ldapPropertiesMapper.getUserActiveAttribute()).thenReturn( "pwdAccountLockedTime" ); + when(ldapPropertiesMapper.getUserActiveValue()).thenReturn(""); + when(ldapPropertiesMapper.getUserInactiveValue()).thenReturn( "000001010000Z" ); } @Test @@ -104,7 +110,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(givenName=joe*)(sn=joe*)(displayName=joe*)))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(givenName=joe*)(sn=joe*)(displayName=joe*))(!(pwdAccountLockedTime=000001010000Z))))" , filter); } @Test @@ -116,14 +122,16 @@ assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))" , filter); } - @Test(expected = NullResultException.class) + @Test public void testSearchInactiveUsers() throws NullResultException { EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching( false )).returningAtMost(10); - queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + String filter = ldapQuery.encode(); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(pwdAccountLockedTime=000001010000Z))" , filter); } - - @Test(expected = NullResultException.class) + @Test + @Ignore( "Those nested results no longer return null " ) public void testSearchInactiveUsersViaNestingReturningNullResult() throws NullResultException { EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.anyOf( Combine.allOf( @@ -136,10 +144,14 @@ ) )).returningAtMost(10); - queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + String filter = ldapQuery.encode(); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))" , filter); + } @Test + @Ignore( "This test is no longer applicable since the nested conditions don't return everything" ) public void testSearchActiveUsersViaNestingReturningEverything() throws NullResultException { EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.allOf( Combine.anyOf( @@ -174,7 +186,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(sn=b*)(givenName=b*)))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(sn=b*)(givenName=b*))))" , filter); } @Test @@ -203,7 +215,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(objectCategory=Person)(sAMAccountName=*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(!(pwdAccountLockedTime=000001010000Z)))" , filter); } @Test @@ -216,7 +228,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(sAMAccountName=*bob*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(!(pwdAccountLockedTime=000001010000Z))(sAMAccountName=*bob*)))" , filter); } @Test

            I would love to see this feature, It is quite annoying having to remove groups for a user that has been removed instead of Jira simply disabling a user based on an attribute.

            Kevin Meijer added a comment - I would love to see this feature, It is quite annoying having to remove groups for a user that has been removed instead of Jira simply disabling a user based on an attribute.

            jcron added a comment -

            This patch (from https://jira.atlassian.com/browse/CWD-2762?focusedCommentId=1993254&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-1993254 ) works fine for us.

            Is it possible to integrate this one into core product?

            We really need this one into Jira, Confluence and Bitbucket!

            jcron added a comment - This patch (from https://jira.atlassian.com/browse/CWD-2762?focusedCommentId=1993254&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-1993254 ) works fine for us. Is it possible to integrate this one into core product? We really need this one into Jira, Confluence and Bitbucket!

            I find it a bit disappointing that this issue is not addresses yet. I created a patch based on the previous patches for a more recent version 3.4.4

            The add attachment button is gone so i added the patch is het in a code block:

            
            

            CWD-2762

            -3.4.4.patch
            Index: atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java	(date 1560350448000)
            @@ -13,8 +13,8 @@
             import com.atlassian.crowd.search.query.entity.restriction.PropertyImpl;
             import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys;
             import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys;
            -
             import org.junit.Before;
            +import org.junit.Ignore;
             import org.junit.Test;
            
             import static org.junit.Assert.assertEquals;
            @@ -48,6 +48,11 @@
                     when(ldapPropertiesMapper.getGroupNameAttribute()).thenReturn("cn");
                     when(ldapPropertiesMapper.getGroupDescriptionAttribute()).thenReturn("description");
                     when(ldapPropertiesMapper.getGroupMemberAttribute()).thenReturn("member");
            +
            +        // test user activation through LDAP Password Policy (http://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
            +        when(ldapPropertiesMapper.getUserActiveAttribute()).thenReturn("pwdAccountLockedTime");
            +        when(ldapPropertiesMapper.getUserActiveValue()).thenReturn("");
            +        when(ldapPropertiesMapper.getUserInactiveValue()).thenReturn("000001010000Z");
                 }
            
                 @Test
            @@ -104,7 +109,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
            
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(givenName=joe*)(sn=joe*)(displayName=joe*)))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(givenName=joe*)(sn=joe*)(displayName=joe*))(!(pwdAccountLockedTime=000001010000Z))))", filter);
                 }
            
                 @Test
            @@ -116,14 +121,16 @@
                     assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))", filter);
                 }
            
            -    @Test(expected = NullResultException.class)
            +    @Test
                 public void testSearchInactiveUsers() throws NullResultException {
                     EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching(false)).returningAtMost(10);
            
            -        queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        String filter = ldapQuery.encode();
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(pwdAccountLockedTime=000001010000Z))", filter);
                 }
            
            -    @Test(expected = NullResultException.class)
            +    @Test
                 public void testSearchInactiveUsersViaNestingReturningNullResult() throws NullResultException {
                     EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.anyOf(
                             Combine.allOf(
            @@ -136,7 +143,11 @@
                             )
                     )).returningAtMost(10);
            
            -        queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
            +        String filter = ldapQuery.encode();
            +        //assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(pwdAccountLockedTime=000001010000Z)(givenName=b*))))", filter);
            +
                 }
            
                 @Test
            @@ -155,7 +166,8 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
            
            -        assertEquals("(&(objectCategory=Person)(sAMAccountName=*))", filter);
            +//        assertEquals("(&(objectCategory=Person)(sAMAccountName=*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(!(pwdAccountLockedTime=000001010000Z))(displayName=b*))(|(!(pwdAccountLockedTime=000001010000Z))(givenName=b*))))", filter);
                 }
            
                 @Test
            @@ -174,7 +186,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
            
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(sn=b*)(givenName=b*)))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(sn=b*)(givenName=b*))))", filter);
                 }
            
                 @Test
            @@ -203,7 +215,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
            
            -        assertEquals("(&(objectCategory=Person)(sAMAccountName=*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(!(pwdAccountLockedTime=000001010000Z)))", filter);
                 }
            
                 @Test
            @@ -216,7 +228,7 @@
                     LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper);
                     String filter = ldapQuery.encode();
            
            -        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(sAMAccountName=*bob*))", filter);
            +        assertEquals("(&(&(objectCategory=Person)(sAMAccountName=*))(&(!(pwdAccountLockedTime=000001010000Z))(sAMAccountName=*bob*)))", filter);
                 }
            
                 @Test
            @@ -281,7 +293,8 @@
                     assertEquals("(&(objectCategory=Role)(cn=admins))", filter);
                 }
            
            -    @Test(expected = NullResultException.class)
            +    @Test
            +    @Ignore("Those nested results no longer return null")
                 public void testSearchInactiveGroups() throws NullResultException {
                     EntityQuery<Group> query = QueryBuilder.queryFor(Group.class, EntityDescriptor.group(GroupType.GROUP)).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching(false)).returningAtMost(10);
            
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java	(date 1560348813000)
            @@ -68,6 +68,13 @@
                     // full name
                     putValueInAttributes(populatedUser.getDisplayName(), ldapPropertiesMapper.getUserDisplayNameAttribute(), directoryAttributes);
            
            +        // active is the default case for LDAP, we only need to write something to the LDAP in case the user is not active
            +        if (!populatedUser.isActive()) {
            +            putValueInAttributes(ldapPropertiesMapper.getUserInactiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        } else {
            +            putValueInAttributes(ldapPropertiesMapper.getUserActiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        }
            +
                     // TODO: currently we don't support arbitrary attributes / iconLocation / active
            
                     return directoryAttributes;
            @@ -129,6 +136,22 @@
                 }
            
                 protected boolean getUserActiveFromAttribute(final Attributes directoryAttributes) {
            +        String userActive = DirectoryAttributeRetriever.getValueFromAttributes(ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes);
            +        if (userActive == null) {
            +            if (logger.isDebugEnabled()) {
            +                logger.debug("User active attribute '{}' on user '{}' is not set or could not be evaluated, assuming user is active", ldapPropertiesMapper.getUserActiveAttribute(), getUsernameFromAttributes(directoryAttributes));
            +            }
            +            return true;
            +        }
            +
            +        if (logger.isDebugEnabled()) {
            +            logger.debug("User active attribute '{}' is set to '{}' on user {}", ldapPropertiesMapper.getUserActiveAttribute(), userActive, getUsernameFromAttributes(directoryAttributes));
            +        }
            +        // is user inactive?
            +        if (userActive.equals(ldapPropertiesMapper.getUserInactiveValue())) {
            +            logger.debug("User is inactive");
            +            return false;
            +        }
                     return true;
                 }
            
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java	(date 1560350511000)
            @@ -1,16 +1,30 @@
             package com.atlassian.crowd.directory;
            
            +import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper;
             import com.atlassian.crowd.directory.ldap.credential.EncryptingCredentialEncoder;
             import com.atlassian.crowd.directory.ldap.credential.LDAPCredentialEncoder;
            +import com.atlassian.crowd.directory.ldap.mapper.UserContextMapperConfig;
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper;
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.user.UserActiveMapper;
             import com.atlassian.crowd.model.user.User;
             import com.atlassian.crowd.password.factory.PasswordEncoderFactory;
             import com.atlassian.crowd.search.ldap.LDAPQueryTranslater;
             import com.atlassian.crowd.util.InstanceFactory;
             import com.atlassian.event.api.EventPublisher;
            +import com.google.common.collect.ImmutableList;
            +import com.google.common.collect.ImmutableList.Builder;
            
             import javax.naming.directory.Attributes;
            +import java.util.HashMap;
            +import java.util.List;
            +import java.util.Map;
            
             public class OpenLDAP extends RFC4519Directory {
            +
            +    private static final String ATTRIBUTE_LOCKOUT_KEY = "pwdAccountLockedTime";
            +    /** @see <a href="http://linux.die.net/man/5/slapo-ppolicy">man for slapo-ppolicy</a> */
            +    private static final String ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED = "000001010000Z";
            +
                 private final PasswordEncoderFactory passwordEncoderFactory;
            
                 public OpenLDAP(LDAPQueryTranslater ldapQueryTranslater, EventPublisher eventPublisher, InstanceFactory instanceFactory,
            @@ -25,7 +39,7 @@
            
                 @Override
                 public String getDescriptiveName() {
            -        return OpenLDAP.getStaticDirectoryType();
            +        return OpenLDAP.getStaticDirectoryType() + " (extended with support for sync of active flag, CWD-2762)";
                 }
            
                 @Override
            @@ -43,4 +57,31 @@
                 protected void getNewUserDirectorySpecificAttributes(final User user, final Attributes attributes) {
                     addDefaultSnToUserAttributes(attributes, user.getName());
                 }
            +
            +
            +    /**
            +     * As long as the LDAP attributes and values are not configurable, we just add those as hard-coded attributes in here, that they are available within {@link com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper}
            +     * The LDAP attribute keys and values are taken from the de-facto standard "Password Policy for LDAP Directories"
            +     * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy-10">Password Policy for LDAP Directories</a>
            +     * @see <a href="http://linux.die.net/man/5/slapo-ppolicy">man for slapo-ppolicy</a>
            +     */
            +    @Override
            +    public void setAttributes(Map<String, String> attributes) {
            +        Map<String, String> extendedAttributes = new HashMap<String, String>(attributes);
            +
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_ATTRIBUTE_KEY, ATTRIBUTE_LOCKOUT_KEY);
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_VALUE, "");
            +        extendedAttributes.put(LDAPPropertiesMapper.USER_INACTIVE_VALUE, ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED);
            +
            +        super.setAttributes(extendedAttributes);
            +    }
            +
            +    @Override
            +    protected List<AttributeMapper> getCustomUserAttributeMappers(UserContextMapperConfig config) {
            +
            +        Builder<AttributeMapper> builder = ImmutableList.<AttributeMapper>builder();
            +        builder.addAll(super.getCustomUserAttributeMappers(config));
            +        builder.add(new UserActiveMapper(ldapPropertiesMapper.getUserActiveAttribute()));
            +        return builder.build();
            +    }
             }
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java	(date 1557236920000)
            @@ -419,6 +419,22 @@
                     return getAttribute(LDAP_USER_ENCRYPTION_METHOD);
                 }
            
            +    @Override
            +    public String getUserActiveAttribute()
            +    {
            +    	return getAttribute(USER_ACTIVE_ATTRIBUTE_KEY);
            +    }
            +
            +    @Override
            +    public String getUserActiveValue() {
            +    	return getAttribute(USER_ACTIVE_VALUE);
            +    }
            +
            +    @Override
            +    public String getUserInactiveValue() {
            +    	return getAttribute(USER_INACTIVE_VALUE);
            +    }
            +
                 /**
                  * Checks if the configuration of the LDAP directory server uses paged results.
                  *
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java	(date 1557236975000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java	(date 1557236975000)
            @@ -0,0 +1,38 @@
            +package com.atlassian.crowd.directory.ldap.mapper.attribute.user;
            +
            +import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper;
            +import org.springframework.ldap.core.DirContextAdapter;
            +
            +import java.util.Collections;
            +import java.util.Set;
            +
            +public class UserActiveMapper implements AttributeMapper {
            +
            +    private final String userActiveAttribute;
            +
            +    public UserActiveMapper(String userActiveAttribute) {
            +        this.userActiveAttribute = userActiveAttribute;
            +    }
            +
            +    @Override
            +    public String getKey() {
            +        return userActiveAttribute;
            +    }
            +
            +    @Override
            +    public Set<String> getValues(DirContextAdapter ctx) throws Exception {
            +        // this must never return null
            +        String userActiveValue = ctx.getStringAttribute(getKey());
            +        if (userActiveValue != null) {
            +            return Collections.singleton(userActiveValue);
            +        } else {
            +            return Collections.emptySet();
            +        }
            +    }
            +
            +    @Override
            +    public Set<String> getRequiredLdapAttributes() {
            +        return Collections.singleton(getKey());
            +    }
            +
            +}
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java	(date 1560350791000)
            @@ -5,6 +5,7 @@
            
             import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper;
             import com.atlassian.crowd.embedded.api.SearchRestriction;
            +import com.atlassian.crowd.search.Entity;
             import com.atlassian.crowd.search.EntityDescriptor;
             import com.atlassian.crowd.search.query.entity.EntityQuery;
             import com.atlassian.crowd.search.query.entity.restriction.BooleanRestriction;
            @@ -13,12 +14,13 @@
             import com.atlassian.crowd.search.query.entity.restriction.PropertyRestriction;
             import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys;
             import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys;
            -
            +import org.apache.commons.lang3.StringUtils;
             import org.springframework.ldap.filter.AndFilter;
             import org.springframework.ldap.filter.BinaryLogicalFilter;
             import org.springframework.ldap.filter.EqualsFilter;
             import org.springframework.ldap.filter.Filter;
             import org.springframework.ldap.filter.LikeFilter;
            +import org.springframework.ldap.filter.NotFilter;
             import org.springframework.ldap.filter.NotPresentFilter;
             import org.springframework.ldap.filter.OrFilter;
             import org.springframework.ldap.filter.PresentFilter;
            @@ -127,16 +129,35 @@
            
                 protected Filter booleanTermRestrictionAsFilter(final EntityDescriptor entityDescriptor, final PropertyRestriction<Boolean> termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) {
                     // if boolean term restrictions are for anything other than the group/user active flag, then throw exception
            -        if (!termRestriction.getProperty().equals(GroupTermKeys.ACTIVE) && !termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) {
            -            throw new IllegalArgumentException("Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported");
            -        } else {
            +        if (entityDescriptor.getEntityType() == Entity.USER && termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) {
            +            if (StringUtils.isEmpty(ldapPropertiesMapper.getUserActiveAttribute())) {
            +                if (termRestriction.getValue()) {
            +                    // groups are always active = true, so no need to add a restriction
            +                    return new EverythingResult();
            +                } else {
            +                    // no group is ever active = false, so no results
            +                    return new NothingResult();
            +                }
            +            } else {
            +                if (termRestriction.getValue()) {
            +                    // querying for enabled users
            +                    return new NotFilter(new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue()));
            +                } else {
            +                    // querying for disabled users
            +                    return new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue());
            +                }
            +            }
            +        } else if (entityDescriptor.getEntityType() == Entity.GROUP && termRestriction.getProperty().equals(GroupTermKeys.ACTIVE)) {
                         if (termRestriction.getValue()) {
            -                // everything is active = true, so no need to add a restriction
            +                // groups are always active = true, so no need to add a restriction
                             return new EverythingResult();
                         } else {
            -                // nothing is active = false, so need to
            +                // no group is ever active = false, so no results
                             return new NothingResult();
                         }
            +        } else {
            +            // if boolean term restrictions are for anything other than the group/user active flag, then throw exception
            +            throw new IllegalArgumentException("Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported");
                     }
                 }
            
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java	(date 1560349863000)
            @@ -870,6 +870,10 @@
            
                         try {
                             SearchControls searchControls = getSearchControls(contextMapper, SearchControls.OBJECT_SCOPE);
            +                if (contextMapper instanceof ContextMapperWithRequiredAttributes<?>) {
            +                    logger.debug("Return required attributes {} for findEntityById", (((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes()));
            +                    searchControls.setReturningAttributes(toArray(((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes()));
            +                }
                             searchControls.setTimeLimit(ldapPropertiesMapper.getSearchTimeLimit());
                             entities = ldapTemplate.search(asLdapName(dn, "DN: " + dn, entityClass), filter, searchControls, contextMapper);
                         } catch (NameNotFoundException e) {
            @@ -971,6 +975,30 @@
                         modificationItems.add(displayNameMod);
                     }
            
            +        // support enabling/disabling users
            +        if (supportsInactiveAccounts()) {
            +            ModificationItem activeModItem = null;
            +            if (!currentUser.isActive() && userTemplate.isActive()) {
            +                // try to activate user in case it is currently inactive
            +                String newValue = ldapPropertiesMapper.getUserActiveValue();
            +                if (StringUtils.isEmpty(newValue)) {
            +                    // rather than modifying an attribute we completely remove it
            +                    activeModItem = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute(ldapPropertiesMapper.getUserActiveAttribute()));
            +                } else {
            +                    // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value
            +                    activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed", newValue);
            +                }
            +            } else if (currentUser.isActive() && !userTemplate.isActive()) {
            +                // try to deactivate user in case it is currently active
            +                String newValue = ldapPropertiesMapper.getUserInactiveValue();
            +                // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value
            +                activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed", newValue);
            +            }
            +            if (activeModItem != null)
            +            {
            +                modificationItems.add(activeModItem);
            +            }
            +        }
                     return modificationItems;
                 }
            
            @@ -1545,7 +1573,7 @@
                  */
                 @Override
                 public boolean supportsInactiveAccounts() {
            -        return false;
            +        return StringUtils.isNotBlank(ldapPropertiesMapper.getUserActiveAttribute());
                 }
            
                 @Override
            Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java	(date 1560350010000)
            @@ -176,6 +176,21 @@
                  */
                 String USER_PASSWORD_KEY = "ldap.user.password";
            
            +    /**
            +     * Attribute key for the LDAP principal active attribute.
            +     */
            +    String USER_ACTIVE_ATTRIBUTE_KEY = "ldap.user.active.attribute";
            +
            +    /**
            +     * Attribute key for the LDAP value which stands for active users. In contrast to {@link #USER_INACTIVE_VALUE} this is only considered for writing to the LDAP, not for evaluation!
            +     */
            +    String USER_ACTIVE_VALUE = "ldap.user.active";
            +
            +    /**
            +     * Attribute key for the LDAP value which stands for inactive users. This is used for checking whether an account is active and also for persisting that information within the LDAP.
            +     */
            +    String USER_INACTIVE_VALUE = "ldap.user.inactive";
            +
                 /**
                  * Attribute key for the LDAP paged results attribute.
                  */
            @@ -380,6 +395,22 @@
            
                 String getUserEncryptionMethod();
            
            +    /**
            +     * @return the LDAP attribute in which the users active state is stored. Derived from {@link #USER_ACTIVE_ATTRIBUTE_KEY}.
            +     */
            +    String getUserActiveAttribute();
            +
            +    /**
            +     * @return the LDAP value which stands for an active user (should only be used for turning a user from inactive to active, i.e. for writing to LDAP, but not for evaluating the state). Derived from {@link #USER_ACTIVE_VALUE}}
            +     */
            +    String getUserActiveValue();
            +
            +    /**
            +     * @return the LDAP value which stands for an inactive user (should only be used for turning a user from active to inactive, i.e. for writing to LDAP, as well as for evaluating the current state). Derived from {@link #USER_INACTIVE_VALUE}}
            +     */
            +    String getUserInactiveValue();
            +
            +
                 boolean isPagedResultsControl();
            
                 int getPagedResultsSize();
            @@ -431,7 +462,7 @@
                  * compatible method for standardising DNs when mapping object DNs and
                  * and memberDNs (value = <code>false</code>); or if we can use a more
                  * efficient but relaxed form of standardisation (value = <code>true</code>).
            -     *
            +     * <p>
                  * See <code>DNStandardiser</code> for more information.
                  *
                  * @return <code>false</code> if proper standardisation is required.
            Index: atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties
            IDEA additional info:
            Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
            <+>UTF-8
            ===================================================================
            --- atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties	(date 1557236232000)
            +++ atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties	(date 1560346799000)
            @@ -24,6 +24,11 @@
             ldap.user.group         memberOf
             ldap.user.password      userPassword
            
            +# active properties (according to http://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
            +ldap.user.active.attribute	pwdAccountLockedTime
            +ldap.user.active
            +ldap.user.inactive		000001010000Z
            +
             # generic options
             ldap.pagedresults               false
             ldap.relaxed.dn.standardisation true
            
            

            Pieter Bruining added a comment - I find it a bit disappointing that this issue is not addresses yet. I created a patch based on the previous patches for a more recent version 3.4.4 The add attachment button is gone so i added the patch is het in a code block: CWD-2762 -3.4.4.patch Index: atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/test/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImplTest.java (date 1560350448000) @@ -13,8 +13,8 @@ import com.atlassian.crowd.search.query.entity.restriction.PropertyImpl; import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys; import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys; - import org.junit.Before; + import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -48,6 +48,11 @@ when(ldapPropertiesMapper.getGroupNameAttribute()).thenReturn( "cn" ); when(ldapPropertiesMapper.getGroupDescriptionAttribute()).thenReturn( "description" ); when(ldapPropertiesMapper.getGroupMemberAttribute()).thenReturn( "member" ); + + // test user activation through LDAP Password Policy (http://tools.ietf.org/html/draft-behera-ldap-password-policy-10) + when(ldapPropertiesMapper.getUserActiveAttribute()).thenReturn( "pwdAccountLockedTime" ); + when(ldapPropertiesMapper.getUserActiveValue()).thenReturn(""); + when(ldapPropertiesMapper.getUserInactiveValue()).thenReturn( "000001010000Z" ); } @Test @@ -104,7 +109,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(givenName=joe*)(sn=joe*)(displayName=joe*)))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(givenName=joe*)(sn=joe*)(displayName=joe*))(!(pwdAccountLockedTime=000001010000Z))))" , filter); } @Test @@ -116,14 +121,16 @@ assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))" , filter); } - @Test(expected = NullResultException.class) + @Test public void testSearchInactiveUsers() throws NullResultException { EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching( false )).returningAtMost(10); - queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + String filter = ldapQuery.encode(); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(pwdAccountLockedTime=000001010000Z))" , filter); } - @Test(expected = NullResultException.class) + @Test public void testSearchInactiveUsersViaNestingReturningNullResult() throws NullResultException { EntityQuery<User> query = QueryBuilder.queryFor(User.class, EntityDescriptor.user()).with(Combine.anyOf( Combine.allOf( @@ -136,7 +143,11 @@ ) )).returningAtMost(10); - queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); + String filter = ldapQuery.encode(); + //assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(entryUUID=a1b2))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(pwdAccountLockedTime=000001010000Z)(givenName=b*))))" , filter); + } @Test @@ -155,7 +166,8 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(objectCategory=Person)(sAMAccountName=*))" , filter); + // assertEquals( "(&(objectCategory=Person)(sAMAccountName=*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(|(!(pwdAccountLockedTime=000001010000Z))(displayName=b*))(|(!(pwdAccountLockedTime=000001010000Z))(givenName=b*))))" , filter); } @Test @@ -174,7 +186,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(sn=b*)(givenName=b*)))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(|(&(pwdAccountLockedTime=000001010000Z)(displayName=b*))(&(sn=b*)(givenName=b*))))" , filter); } @Test @@ -203,7 +215,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(objectCategory=Person)(sAMAccountName=*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(!(pwdAccountLockedTime=000001010000Z)))" , filter); } @Test @@ -216,7 +228,7 @@ LDAPQuery ldapQuery = queryTranslater.asLDAPFilter(query, ldapPropertiesMapper); String filter = ldapQuery.encode(); - assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(sAMAccountName=*bob*))" , filter); + assertEquals( "(&(&(objectCategory=Person)(sAMAccountName=*))(&(!(pwdAccountLockedTime=000001010000Z))(sAMAccountName=*bob*)))" , filter); } @Test @@ -281,7 +293,8 @@ assertEquals( "(&(objectCategory=Role)(cn=admins))" , filter); } - @Test(expected = NullResultException.class) + @Test + @Ignore( "Those nested results no longer return null " ) public void testSearchInactiveGroups() throws NullResultException { EntityQuery<Group> query = QueryBuilder.queryFor(Group.class, EntityDescriptor.group(GroupType.GROUP)).with(Restriction.on(UserTermKeys.ACTIVE).exactlyMatching( false )).returningAtMost(10); Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/entity/LDAPUserAttributesMapper.java (date 1560348813000) @@ -68,6 +68,13 @@ // full name putValueInAttributes(populatedUser.getDisplayName(), ldapPropertiesMapper.getUserDisplayNameAttribute(), directoryAttributes); + // active is the default case for LDAP, we only need to write something to the LDAP in case the user is not active + if (!populatedUser.isActive()) { + putValueInAttributes(ldapPropertiesMapper.getUserInactiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + } else { + putValueInAttributes(ldapPropertiesMapper.getUserActiveValue(), ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + } + // TODO: currently we don't support arbitrary attributes / iconLocation / active return directoryAttributes; @@ -129,6 +136,22 @@ } protected boolean getUserActiveFromAttribute( final Attributes directoryAttributes) { + String userActive = DirectoryAttributeRetriever.getValueFromAttributes(ldapPropertiesMapper.getUserActiveAttribute(), directoryAttributes); + if (userActive == null ) { + if (logger.isDebugEnabled()) { + logger.debug( "User active attribute '{}' on user '{}' is not set or could not be evaluated, assuming user is active" , ldapPropertiesMapper.getUserActiveAttribute(), getUsernameFromAttributes(directoryAttributes)); + } + return true ; + } + + if (logger.isDebugEnabled()) { + logger.debug( "User active attribute '{}' is set to '{}' on user {}" , ldapPropertiesMapper.getUserActiveAttribute(), userActive, getUsernameFromAttributes(directoryAttributes)); + } + // is user inactive? + if (userActive.equals(ldapPropertiesMapper.getUserInactiveValue())) { + logger.debug( "User is inactive" ); + return false ; + } return true ; } Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/OpenLDAP.java (date 1560350511000) @@ -1,16 +1,30 @@ package com.atlassian.crowd.directory; + import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper; import com.atlassian.crowd.directory.ldap.credential.EncryptingCredentialEncoder; import com.atlassian.crowd.directory.ldap.credential.LDAPCredentialEncoder; + import com.atlassian.crowd.directory.ldap.mapper.UserContextMapperConfig; + import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper; + import com.atlassian.crowd.directory.ldap.mapper.attribute.user.UserActiveMapper; import com.atlassian.crowd.model.user.User; import com.atlassian.crowd.password.factory.PasswordEncoderFactory; import com.atlassian.crowd.search.ldap.LDAPQueryTranslater; import com.atlassian.crowd.util.InstanceFactory; import com.atlassian.event.api.EventPublisher; + import com.google.common.collect.ImmutableList; + import com.google.common.collect.ImmutableList.Builder; import javax.naming.directory.Attributes; + import java.util.HashMap; + import java.util.List; + import java.util.Map; public class OpenLDAP extends RFC4519Directory { + + private static final String ATTRIBUTE_LOCKOUT_KEY = "pwdAccountLockedTime" ; + /** @see <a href= "http: //linux.die.net/man/5/slapo-ppolicy" >man for slapo-ppolicy</a> */ + private static final String ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED = "000001010000Z" ; + private final PasswordEncoderFactory passwordEncoderFactory; public OpenLDAP(LDAPQueryTranslater ldapQueryTranslater, EventPublisher eventPublisher, InstanceFactory instanceFactory, @@ -25,7 +39,7 @@ @Override public String getDescriptiveName() { - return OpenLDAP.getStaticDirectoryType(); + return OpenLDAP.getStaticDirectoryType() + " (extended with support for sync of active flag, CWD-2762)" ; } @Override @@ -43,4 +57,31 @@ protected void getNewUserDirectorySpecificAttributes( final User user, final Attributes attributes) { addDefaultSnToUserAttributes(attributes, user.getName()); } + + + /** + * As long as the LDAP attributes and values are not configurable, we just add those as hard-coded attributes in here, that they are available within {@link com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper} + * The LDAP attribute keys and values are taken from the de-facto standard "Password Policy for LDAP Directories" + * @see <a href= "http: //tools.ietf.org/html/draft-behera-ldap-password-policy-10" >Password Policy for LDAP Directories</a> + * @see <a href= "http: //linux.die.net/man/5/slapo-ppolicy" >man for slapo-ppolicy</a> + */ + @Override + public void setAttributes(Map< String , String > attributes) { + Map< String , String > extendedAttributes = new HashMap< String , String >(attributes); + + extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_ATTRIBUTE_KEY, ATTRIBUTE_LOCKOUT_KEY); + extendedAttributes.put(LDAPPropertiesMapper.USER_ACTIVE_VALUE, ""); + extendedAttributes.put(LDAPPropertiesMapper.USER_INACTIVE_VALUE, ATTRIBUTE_LOCKOUT_VALUE_PERMANENTLY_LOCKED); + + super .setAttributes(extendedAttributes); + } + + @Override + protected List<AttributeMapper> getCustomUserAttributeMappers(UserContextMapperConfig config) { + + Builder<AttributeMapper> builder = ImmutableList.<AttributeMapper>builder(); + builder.addAll( super .getCustomUserAttributeMappers(config)); + builder.add( new UserActiveMapper(ldapPropertiesMapper.getUserActiveAttribute())); + return builder.build(); + } } Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapperImpl.java (date 1557236920000) @@ -419,6 +419,22 @@ return getAttribute(LDAP_USER_ENCRYPTION_METHOD); } + @Override + public String getUserActiveAttribute() + { + return getAttribute(USER_ACTIVE_ATTRIBUTE_KEY); + } + + @Override + public String getUserActiveValue() { + return getAttribute(USER_ACTIVE_VALUE); + } + + @Override + public String getUserInactiveValue() { + return getAttribute(USER_INACTIVE_VALUE); + } + /** * Checks if the configuration of the LDAP directory server uses paged results. * Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java (date 1557236975000) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/mapper/attribute/user/UserActiveMapper.java (date 1557236975000) @@ -0,0 +1,38 @@ + package com.atlassian.crowd.directory.ldap.mapper.attribute.user; + + import com.atlassian.crowd.directory.ldap.mapper.attribute.AttributeMapper; + import org.springframework.ldap.core.DirContextAdapter; + + import java.util.Collections; + import java.util.Set; + + public class UserActiveMapper implements AttributeMapper { + + private final String userActiveAttribute; + + public UserActiveMapper( String userActiveAttribute) { + this .userActiveAttribute = userActiveAttribute; + } + + @Override + public String getKey() { + return userActiveAttribute; + } + + @Override + public Set< String > getValues(DirContextAdapter ctx) throws Exception { + // this must never return null + String userActiveValue = ctx.getStringAttribute(getKey()); + if (userActiveValue != null ) { + return Collections.singleton(userActiveValue); + } else { + return Collections.emptySet(); + } + } + + @Override + public Set< String > getRequiredLdapAttributes() { + return Collections.singleton(getKey()); + } + +} Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/search/ldap/LDAPQueryTranslaterImpl.java (date 1560350791000) @@ -5,6 +5,7 @@ import com.atlassian.crowd.directory.ldap.LDAPPropertiesMapper; import com.atlassian.crowd.embedded.api.SearchRestriction; + import com.atlassian.crowd.search.Entity; import com.atlassian.crowd.search.EntityDescriptor; import com.atlassian.crowd.search.query.entity.EntityQuery; import com.atlassian.crowd.search.query.entity.restriction.BooleanRestriction; @@ -13,12 +14,13 @@ import com.atlassian.crowd.search.query.entity.restriction.PropertyRestriction; import com.atlassian.crowd.search.query.entity.restriction.constants.GroupTermKeys; import com.atlassian.crowd.search.query.entity.restriction.constants.UserTermKeys; - + import org.apache.commons.lang3.StringUtils; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.BinaryLogicalFilter; import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.filter.Filter; import org.springframework.ldap.filter.LikeFilter; + import org.springframework.ldap.filter.NotFilter; import org.springframework.ldap.filter.NotPresentFilter; import org.springframework.ldap.filter.OrFilter; import org.springframework.ldap.filter.PresentFilter; @@ -127,16 +129,35 @@ protected Filter booleanTermRestrictionAsFilter( final EntityDescriptor entityDescriptor, final PropertyRestriction< Boolean > termRestriction, LDAPPropertiesMapper ldapPropertiesMapper) { // if boolean term restrictions are for anything other than the group/user active flag, then throw exception - if (!termRestriction.getProperty().equals(GroupTermKeys.ACTIVE) && !termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) { - throw new IllegalArgumentException( " Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported" ); - } else { + if (entityDescriptor.getEntityType() == Entity.USER && termRestriction.getProperty().equals(UserTermKeys.ACTIVE)) { + if (StringUtils.isEmpty(ldapPropertiesMapper.getUserActiveAttribute())) { + if (termRestriction.getValue()) { + // groups are always active = true , so no need to add a restriction + return new EverythingResult(); + } else { + // no group is ever active = false , so no results + return new NothingResult(); + } + } else { + if (termRestriction.getValue()) { + // querying for enabled users + return new NotFilter( new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue())); + } else { + // querying for disabled users + return new EqualsFilter(ldapPropertiesMapper.getUserActiveAttribute(), ldapPropertiesMapper.getUserInactiveValue()); + } + } + } else if (entityDescriptor.getEntityType() == Entity.GROUP && termRestriction.getProperty().equals(GroupTermKeys.ACTIVE)) { if (termRestriction.getValue()) { - // everything is active = true , so no need to add a restriction + // groups are always active = true , so no need to add a restriction return new EverythingResult(); } else { - // nothing is active = false , so need to + // no group is ever active = false , so no results return new NothingResult(); } + } else { + // if boolean term restrictions are for anything other than the group/user active flag, then throw exception + throw new IllegalArgumentException( " Boolean restrictions for property " + termRestriction.getProperty().getPropertyName() + " are not supported" ); } } Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/SpringLDAPConnector.java (date 1560349863000) @@ -870,6 +870,10 @@ try { SearchControls searchControls = getSearchControls(contextMapper, SearchControls.OBJECT_SCOPE); + if (contextMapper instanceof ContextMapperWithRequiredAttributes<?>) { + logger.debug( "Return required attributes {} for findEntityById" , (((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes())); + searchControls.setReturningAttributes(toArray(((ContextMapperWithRequiredAttributes<?>)contextMapper).getRequiredLdapAttributes())); + } searchControls.setTimeLimit(ldapPropertiesMapper.getSearchTimeLimit()); entities = ldapTemplate.search(asLdapName(dn, "DN: " + dn, entityClass), filter, searchControls, contextMapper); } catch (NameNotFoundException e) { @@ -971,6 +975,30 @@ modificationItems.add(displayNameMod); } + // support enabling/disabling users + if (supportsInactiveAccounts()) { + ModificationItem activeModItem = null ; + if (!currentUser.isActive() && userTemplate.isActive()) { + // try to activate user in case it is currently inactive + String newValue = ldapPropertiesMapper.getUserActiveValue(); + if (StringUtils.isEmpty(newValue)) { + // rather than modifying an attribute we completely remove it + activeModItem = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute(ldapPropertiesMapper.getUserActiveAttribute())); + } else { + // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value + activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed" , newValue); + } + } else if (currentUser.isActive() && !userTemplate.isActive()) { + // try to deactivate user in case it is currently active + String newValue = ldapPropertiesMapper.getUserInactiveValue(); + // TODO: getting the real old value from LDAP is not easily possible here, just always use a REPLACE by setting the old value to something else than new value + activeModItem = createModificationItem(ldapPropertiesMapper.getUserActiveAttribute(), newValue + "changed" , newValue); + } + if (activeModItem != null ) + { + modificationItems.add(activeModItem); + } + } return modificationItems; } @@ -1545,7 +1573,7 @@ */ @Override public boolean supportsInactiveAccounts() { - return false ; + return StringUtils.isNotBlank(ldapPropertiesMapper.getUserActiveAttribute()); } @Override Index: atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/main/java/com/atlassian/crowd/directory/ldap/LDAPPropertiesMapper.java (date 1560350010000) @@ -176,6 +176,21 @@ */ String USER_PASSWORD_KEY = "ldap.user.password" ; + /** + * Attribute key for the LDAP principal active attribute. + */ + String USER_ACTIVE_ATTRIBUTE_KEY = "ldap.user.active.attribute" ; + + /** + * Attribute key for the LDAP value which stands for active users. In contrast to {@link #USER_INACTIVE_VALUE} this is only considered for writing to the LDAP, not for evaluation! + */ + String USER_ACTIVE_VALUE = "ldap.user.active" ; + + /** + * Attribute key for the LDAP value which stands for inactive users. This is used for checking whether an account is active and also for persisting that information within the LDAP. + */ + String USER_INACTIVE_VALUE = "ldap.user.inactive" ; + /** * Attribute key for the LDAP paged results attribute. */ @@ -380,6 +395,22 @@ String getUserEncryptionMethod(); + /** + * @ return the LDAP attribute in which the users active state is stored. Derived from {@link #USER_ACTIVE_ATTRIBUTE_KEY}. + */ + String getUserActiveAttribute(); + + /** + * @ return the LDAP value which stands for an active user (should only be used for turning a user from inactive to active, i.e. for writing to LDAP, but not for evaluating the state). Derived from {@link #USER_ACTIVE_VALUE}} + */ + String getUserActiveValue(); + + /** + * @ return the LDAP value which stands for an inactive user (should only be used for turning a user from active to inactive, i.e. for writing to LDAP, as well as for evaluating the current state). Derived from {@link #USER_INACTIVE_VALUE}} + */ + String getUserInactiveValue(); + + boolean isPagedResultsControl(); int getPagedResultsSize(); @@ -431,7 +462,7 @@ * compatible method for standardising DNs when mapping object DNs and * and memberDNs (value = <code> false </code>); or if we can use a more * efficient but relaxed form of standardisation (value = <code> true </code>). - * + * <p> * See <code>DNStandardiser</code> for more information. * * @ return <code> false </code> if proper standardisation is required. Index: atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties (date 1557236232000) +++ atlassian-crowd/components/crowd-ldap/src/main/resources/com/atlassian/crowd/integration/directory/openldap.properties (date 1560346799000) @@ -24,6 +24,11 @@ ldap.user.group memberOf ldap.user.password userPassword +# active properties (according to http: //tools.ietf.org/html/draft-behera-ldap-password-policy-10) +ldap.user.active.attribute pwdAccountLockedTime +ldap.user.active +ldap.user.inactive 000001010000Z + # generic options ldap.pagedresults false ldap.relaxed.dn.standardisation true

            I added the updated patch for a more recent version: CWD-2762-3.1.2.patch

            Deleted Account (Inactive) added a comment - I added the updated patch for a more recent version:  CWD-2762-3.1.2.patch

            I attached the modified patch for 2.11.1 which supports OpenLDAP: CWD-2762-2.11.1.patch

            Konrad Windszus added a comment - I attached the modified patch for 2.11.1 which supports OpenLDAP: CWD-2762-2.11.1.patch

            Thanks @gviana!

            Jason @ FPC added a comment - Thanks @gviana!

              Unassigned Unassigned
              0de2fc390d16 jh
              Affected customers:
              52 This affects my team
              Watchers:
              50 Start watching this issue

                Created:
                Updated: