diff --git a/config/alfresco/subsystems/Authentication/common-ldap-context.xml b/config/alfresco/subsystems/Authentication/common-ldap-context.xml index 814c0df702..f952714f00 100644 --- a/config/alfresco/subsystems/Authentication/common-ldap-context.xml +++ b/config/alfresco/subsystems/Authentication/common-ldap-context.xml @@ -93,6 +93,9 @@ + + + + + + + + + + ${ldap.synchronization.userAccountStatusProperty} + diff --git a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication-context.xml b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication-context.xml index 05442f1f66..f42893167f 100644 --- a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication-context.xml +++ b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication-context.xml @@ -7,4 +7,8 @@ defaults --> + + + + \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties index c719ba8d9b..ffbde08c1a 100644 --- a/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties @@ -165,4 +165,10 @@ ldap.pooling.com.sun.jndi.ldap.connect.pool.protocol=plain ldap.pooling.com.sun.jndi.ldap.connect.pool.timeout= # The string representation of an integer that represents the number of milliseconds to specify how long to wait for a pooled connection. If you omit this property, the application will wait indefinitely. -ldap.pooling.com.sun.jndi.ldap.connect.timeout= \ No newline at end of file +ldap.pooling.com.sun.jndi.ldap.connect.timeout= + +# LDAP-AD property name for user enabled/disabled status +ldap.synchronization.userAccountStatusProperty=userAccountControl + +# The Account Status Interpreter bean name +ldap.synchronization.userAccountStatusInterpreter=ldapadUserAccountStatusInterpreter diff --git a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml index 05442f1f66..6bbaeb3699 100644 --- a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml +++ b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication-context.xml @@ -7,4 +7,14 @@ defaults --> + + + + + ${ldap.synchronization.disabledAccountPropertyValue} + + + ${ldap.synchronization.disabledAccountPropertyValueCanBeNull} + + \ No newline at end of file diff --git a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties index 4fdc64b6db..3c509c0bdc 100644 --- a/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties +++ b/config/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties @@ -172,4 +172,20 @@ ldap.pooling.com.sun.jndi.ldap.connect.pool.timeout= # The string representation of an integer that represents the number of milliseconds to specify how long to wait for a pooled connection. # Empty value means the application will wait indefinitely. -ldap.pooling.com.sun.jndi.ldap.connect.timeout= \ No newline at end of file +ldap.pooling.com.sun.jndi.ldap.connect.timeout= + +# Enabled/disabled status - there is no standard way to check for this; +# "nsAccountLock" is used by most NDS derived directory systems (Oracle / Red Hat / 389 DS); +# For OpenLDAP you may want to specify "pwdAccountLockedTime" instead +ldap.synchronization.userAccountStatusProperty=nsAccountLock + +# Expected value for disabled account; +# For NDS directory servers: nsAccountLock=true +# For OpenLDAP: pwdAccountLockedTime=000001010000Z +ldap.synchronization.disabledAccountPropertyValue=true + +# Some directory servers may not send a status value at all if account is enabled +ldap.synchronization.disabledAccountPropertyValueCanBeNull=true + +# The Account Status Interpreter bean name +ldap.synchronization.userAccountStatusInterpreter=ldapUserAccountStatusInterpreter diff --git a/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml b/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml index acae1ea4a4..f76aaa578d 100644 --- a/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml +++ b/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml @@ -84,6 +84,12 @@ ${synchronization.syncDelete} + + ${synchronization.externalUserControl} + + + ${synchronization.externalUserControlSubsystemName} + diff --git a/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties b/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties index c39f598559..2490acf60e 100644 --- a/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties +++ b/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties @@ -33,4 +33,10 @@ synchronization.workerThreads=1 synchronization.allowDeletions=true # For large LDAP directories the delete query is expensive and time consuming, needing to read the entire LDAP directory. -synchronization.syncDelete=true \ No newline at end of file +synchronization.syncDelete=true + +# external setting (LDAP systems) - whether users can be enabled; if false then users have to be explicitly disabled in Alfresco +synchronization.externalUserControl=false + +# Subsystem that will handle the external user control +synchronization.externalUserControlSubsystemName= diff --git a/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java index 4357aea5ac..9e4b8e30a5 100644 --- a/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java +++ b/source/java/org/alfresco/repo/security/authentication/AuthenticationServiceImpl.java @@ -31,6 +31,7 @@ import java.util.Set; import org.alfresco.repo.management.subsystems.ActivateableBean; import org.alfresco.repo.security.authentication.AuthenticationComponent.UserNameValidationMode; import org.alfresco.repo.tenant.TenantContextHolder; +import org.alfresco.service.cmr.security.PersonService; import org.alfresco.util.Pair; public class AuthenticationServiceImpl extends AbstractAuthenticationService implements ActivateableBean @@ -42,7 +43,14 @@ public class AuthenticationServiceImpl extends AbstractAuthenticationService imp private boolean allowsUserCreation = true; private boolean allowsUserDeletion = true; private boolean allowsUserPasswordChange = true; - + + private PersonService personService; + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + public AuthenticationServiceImpl() { super(); @@ -336,6 +344,11 @@ public class AuthenticationServiceImpl extends AbstractAuthenticationService imp */ public boolean getAuthenticationEnabled(String userName) throws AuthenticationException { + if (personService.personExists(userName)) + { + return personService.isEnabled(userName); + } + return true; } } diff --git a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java index 210f1dddfe..4fe954b60f 100644 --- a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java +++ b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java @@ -848,6 +848,9 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per properties.put(ContentModel.PROP_USERNAME, realUserName); } } + + checkIfPersonShouldBeDisabledAndSetAspect(personNode, properties); + Map update = nodeService.getProperties(personNode); update.putAll(properties); @@ -987,6 +990,8 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per beforeCreateNodeValidationBehaviour.enable(); } + checkIfPersonShouldBeDisabledAndSetAspect(personRef, properties); + if (zones != null) { for (String zone : zones) @@ -1004,6 +1009,26 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per return personRef; } + private void checkIfPersonShouldBeDisabledAndSetAspect(NodeRef person, Map properties) + { + if (properties.get(ContentModel.PROP_ENABLED) != null) + { + boolean isEnabled = Boolean.parseBoolean(properties.get(ContentModel.PROP_ENABLED).toString()); + + if (isEnabled) + { + if (nodeService.hasAspect(person, ContentModel.ASPECT_PERSON_DISABLED)) + { + nodeService.removeAspect(person, ContentModel.ASPECT_PERSON_DISABLED); + } + } + else + { + nodeService.addAspect(person, ContentModel.ASPECT_PERSON_DISABLED, null); + } + } + } + /** * {@inheritDoc} */ diff --git a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java index 6c64a3cf03..9908dcf979 100644 --- a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java +++ b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java @@ -39,6 +39,7 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.authentication.AuthenticatorDeletedEvent; import org.alfresco.repo.security.authority.UnknownAuthorityException; +import org.alfresco.repo.security.sync.ldap.LDAPUserRegistry; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; import org.alfresco.repo.transaction.RetryingTransactionHelper; @@ -65,6 +66,7 @@ import org.springframework.extensions.surf.util.AbstractLifecycleBean; import org.springframework.extensions.surf.util.I18NUtil; import javax.management.*; + import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; @@ -195,6 +197,10 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean private NameChecker nameChecker; private SysAdminParams sysAdminParams; + + private String externalUserControl = ""; + + private String externalUserControlSubsystemName = ""; public void init() { @@ -208,6 +214,16 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean PropertyCheck.mandatory(this, "sysAdminParams", sysAdminParams); } + public void setExternalUserControl(String externalUserControl) + { + this.externalUserControl = externalUserControl; + } + + public void setExternalUserControlSubsystemName(String externalUserControlSubsystemName) + { + this.externalUserControlSubsystemName = externalUserControlSubsystemName; + } + /** * Sets name checker */ @@ -1765,6 +1781,9 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, this.loggingInterval); + + final UserRegistry userRegistryFinalRef = userRegistry; + class PersonWorker extends BaseBatchProcessWorker { private long latestTime; @@ -1790,6 +1809,36 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean HashMap personProperties = new HashMap(person.getProperties()); String personName = personProperties.get(ContentModel.PROP_USERNAME).toString().trim(); personProperties.put(ContentModel.PROP_USERNAME, personName); + + if (Boolean.parseBoolean(ChainingUserRegistrySynchronizer.this.externalUserControl) + && ChainingUserRegistrySynchronizer.this.externalUserControlSubsystemName.equals(zone) + && userRegistryFinalRef instanceof LDAPUserRegistry) + { + try + { + LDAPUserRegistry ldapUserRegistry = (LDAPUserRegistry) userRegistryFinalRef; + + if (ldapUserRegistry.getUserAccountStatusInterpreter() != null) + { + QName propertyNameToCheck = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "userAccountStatusProperty"); + + if (personProperties.get(propertyNameToCheck) != null || ldapUserRegistry.getUserAccountStatusInterpreter().acceptsNullArgument()) + { + boolean isUserAccountDisabled = ldapUserRegistry.getUserAccountStatusInterpreter().isUserAccountDisabled( + personProperties.get(propertyNameToCheck)); + + personProperties.put(ContentModel.PROP_ENABLED, !isUserAccountDisabled); + } + } + } + catch (IllegalArgumentException iae) + { + // Can be thrown by certain implementations of AbstractDirectoryServiceUserAccountStatusInterpreter; + // We'll just log it. + ChainingUserRegistrySynchronizer.logger.debug(iae.getMessage(), iae); + } + } + // for invalid names will throw ConstraintException that will be catched by BatchProcessor$TxnCallback nameChecker.evaluate(personName); Set zones = ChainingUserRegistrySynchronizer.this.authorityService diff --git a/source/java/org/alfresco/repo/security/sync/ldap/AbstractDirectoryServiceUserAccountStatusInterpreter.java b/source/java/org/alfresco/repo/security/sync/ldap/AbstractDirectoryServiceUserAccountStatusInterpreter.java new file mode 100644 index 0000000000..ce3c02c85c --- /dev/null +++ b/source/java/org/alfresco/repo/security/sync/ldap/AbstractDirectoryServiceUserAccountStatusInterpreter.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.sync.ldap; + +import java.io.Serializable; + +public abstract class AbstractDirectoryServiceUserAccountStatusInterpreter +{ + public static final String USER_ACCOUNT_STATUS_NOT_NULL_MESSAGE = "User account status property value must not be null."; + + protected void checkForNullArgument(Serializable arg) + { + if (arg == null) + { + throw new IllegalArgumentException(USER_ACCOUNT_STATUS_NOT_NULL_MESSAGE); + } + } + + /** + * Check if directory server user account status is disabled. + * + * @param userAccountStatusValue + * value to interpret user account status from; + * + * @return true if interpreted as disabled, false otherwise + */ + public abstract boolean isUserAccountDisabled(Serializable userAccountStatusValue) throws IllegalArgumentException; + + /** + * Specify if the particular implementation of + * {@link AbstractDirectoryServiceUserAccountStatusInterpreter#isUserAccountDisabled(Serializable)} + * will accept null. + * + * @return true if accepts null. + */ + public abstract boolean acceptsNullArgument(); +} diff --git a/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserAccountStatusInterpreter.java b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserAccountStatusInterpreter.java new file mode 100644 index 0000000000..c3d4a99595 --- /dev/null +++ b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserAccountStatusInterpreter.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.sync.ldap; + +import java.io.Serializable; + +public class LDAPUserAccountStatusInterpreter extends AbstractDirectoryServiceUserAccountStatusInterpreter +{ + private String disabledAccountPropertyValue = ""; + private boolean acceptNullArgument; + + public void setDisabledAccountPropertyValue(String disabledAccountPropertyValue) + { + this.disabledAccountPropertyValue = disabledAccountPropertyValue; + } + + public void setAcceptNullArgument(boolean acceptNullArgument) + { + this.acceptNullArgument = acceptNullArgument; + } + + @Override + public boolean isUserAccountDisabled(Serializable userAccountStatus) + { + if (!acceptsNullArgument()) + { + checkForNullArgument(userAccountStatus); + } + + return userAccountStatus != null && userAccountStatus.toString().equalsIgnoreCase(disabledAccountPropertyValue); + } + + @Override + public boolean acceptsNullArgument() + { + return acceptNullArgument; + } +} diff --git a/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java index 2d6383eb1e..341df0c010 100644 --- a/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java +++ b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java @@ -191,6 +191,9 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial /** The LDAP generalized time format. */ private DateFormat timestampFormat; + /** The LDAP User Account Status Property Interpreter */ + private AbstractDirectoryServiceUserAccountStatusInterpreter userAccountStatusInterpreter; + /** * Instantiates a new lDAP user registry. */ @@ -505,6 +508,16 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial this.attributeBatchSize = attributeBatchSize; } + public void setUserAccountStatusInterpreter(AbstractDirectoryServiceUserAccountStatusInterpreter userAccountStatusInterpreter) + { + this.userAccountStatusInterpreter = userAccountStatusInterpreter; + } + + public AbstractDirectoryServiceUserAccountStatusInterpreter getUserAccountStatusInterpreter() + { + return userAccountStatusInterpreter; + } + /* * (non-Javadoc) * @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive() diff --git a/source/java/org/alfresco/repo/security/sync/ldap_ad/LDAPADUserAccountStatusInterpreter.java b/source/java/org/alfresco/repo/security/sync/ldap_ad/LDAPADUserAccountStatusInterpreter.java new file mode 100644 index 0000000000..8656099522 --- /dev/null +++ b/source/java/org/alfresco/repo/security/sync/ldap_ad/LDAPADUserAccountStatusInterpreter.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.security.sync.ldap_ad; + +import java.io.Serializable; + +import org.alfresco.repo.security.sync.ldap.AbstractDirectoryServiceUserAccountStatusInterpreter; + +public class LDAPADUserAccountStatusInterpreter extends AbstractDirectoryServiceUserAccountStatusInterpreter +{ + @Override + public boolean isUserAccountDisabled(Serializable userAccountStatusValue) + { + checkForNullArgument(userAccountStatusValue); + + /* + * References: + * https://blogs.technet.microsoft.com/heyscriptingguy/2005/05/12/how-can-i-get-a-list-of-all-the-disabled-user-accounts-in-active-directory + * http://stackoverflow.com/questions/19250969/include-enabled-disabled-account-status-of-ldap-user-in-results/19252033#19252033 + */ + return ((Integer.parseInt(userAccountStatusValue.toString())) & 2) != 0; + } + + @Override + public boolean acceptsNullArgument() + { + return false; + } +} diff --git a/source/test-java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java b/source/test-java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java index b053ccfffc..ca0eedcd71 100644 --- a/source/test-java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java +++ b/source/test-java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java @@ -36,6 +36,10 @@ import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.person.PersonServiceImpl; +import org.alfresco.repo.security.sync.ldap.AbstractDirectoryServiceUserAccountStatusInterpreter; +import org.alfresco.repo.security.sync.ldap.LDAPUserAccountStatusInterpreter; +import org.alfresco.repo.security.sync.ldap.LDAPUserRegistry; +import org.alfresco.repo.security.sync.ldap_ad.LDAPADUserAccountStatusInterpreter; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.NodeRef; @@ -44,6 +48,7 @@ import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.AuthorityType; import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.GUID; import org.alfresco.util.PropertyMap; @@ -407,6 +412,135 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase tearDownTestUsersAndGroups(); } + private class MockLDAPUserRegistry extends LDAPUserRegistry implements IMockUserRegistry + { + MockUserRegistry mockUserRegistry; + + public MockLDAPUserRegistry(MockUserRegistry mockUserRegistry) + { + this.mockUserRegistry = mockUserRegistry; + } + + @Override + public void setActive(boolean active) + { + mockUserRegistry.setActive(active); + } + + @Override + public boolean isActive() + { + return mockUserRegistry.isActive(); + } + + @Override + public Set getPersonMappedProperties() + { + return mockUserRegistry.getPersonMappedProperties(); + } + + @Override + public Collection getPersons(Date modifiedSince) + { + return mockUserRegistry.getPersons(modifiedSince); + } + + @Override + public Collection getPersonNames() + { + return mockUserRegistry.getPersonNames(); + } + + @Override + public Collection getGroupNames() + { + return mockUserRegistry.getGroupNames(); + } + + @Override + public Collection getGroups(Date modifiedSince) + { + return mockUserRegistry.getGroups(modifiedSince); + } + + @Override + public String getZoneId() + { + return mockUserRegistry.getZoneId(); + } + + @Override + public void updateState(Collection persons, Collection groups) + { + mockUserRegistry.updateState(persons, groups); + } + } + + private void testLDAPDisableUserAccount(AbstractDirectoryServiceUserAccountStatusInterpreter userAccountStatusInterpreter, + String enabledAccountPropertyValue, String disabledAccountPropertyValue) throws Exception + { + MockUserRegistry mockUserRegistry = new MockUserRegistry("ldap1", new NodeDescription[] { + newPersonWithUserAccountStatusProperty("EnabledUser", enabledAccountPropertyValue), + newPersonWithUserAccountStatusProperty("DisabledUser", disabledAccountPropertyValue) }, new NodeDescription[] {}); + + MockLDAPUserRegistry mockLDAPUserRegistry = new MockLDAPUserRegistry(mockUserRegistry); + mockLDAPUserRegistry.setUserAccountStatusInterpreter(userAccountStatusInterpreter); + + this.applicationContextManager.setUserRegistries(mockLDAPUserRegistry); + + ChainingUserRegistrySynchronizer chainingSynchronizer = (ChainingUserRegistrySynchronizer) this.synchronizer; + chainingSynchronizer.setExternalUserControl("true"); + chainingSynchronizer.setExternalUserControlSubsystemName("ldap1"); + + this.synchronizer.synchronize(false, false); + + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + NodeRef enabledUserRef = ChainingUserRegistrySynchronizerTest.this.personService.getPerson("EnabledUser", false); + assertFalse(ChainingUserRegistrySynchronizerTest.this.nodeService.hasAspect(enabledUserRef, ContentModel.ASPECT_PERSON_DISABLED)); + + NodeRef disabledUserRef = ChainingUserRegistrySynchronizerTest.this.personService.getPerson("DisabledUser", false); + assertTrue(ChainingUserRegistrySynchronizerTest.this.nodeService.hasAspect(disabledUserRef, ContentModel.ASPECT_PERSON_DISABLED)); + + return null; + } + }, false, true); + } + + public void testLDAPDisableUserAccountWithActiveDirectoryProperty() throws Exception + { + LDAPADUserAccountStatusInterpreter ldapadUserAccountStatusInterpreter = new LDAPADUserAccountStatusInterpreter(); + + // Active Directory enabled account: userAccountControl=512; + // disabled account: userAccountControl=514. + testLDAPDisableUserAccount(ldapadUserAccountStatusInterpreter, "512", "514"); + } + + public void testLDAPDisableUserAccountWithNetscapDSProperty() throws Exception + { + LDAPUserAccountStatusInterpreter ldapUserAccountStatusInterpreter = new LDAPUserAccountStatusInterpreter(); + ldapUserAccountStatusInterpreter.setAcceptNullArgument(true); + + // Netscape Directory Server derivatives (Oracle, Red Hat, 389 DS) + // disabled account property: nsAccountLock=true. + ldapUserAccountStatusInterpreter.setDisabledAccountPropertyValue("true"); + + testLDAPDisableUserAccount(ldapUserAccountStatusInterpreter, null, "true"); + } + + public void testLDAPDisableUserAccountWithOpenLDAPProperty() throws Exception + { + LDAPUserAccountStatusInterpreter ldapUserAccountStatusInterpreter = new LDAPUserAccountStatusInterpreter(); + ldapUserAccountStatusInterpreter.setAcceptNullArgument(true); + + // OpenLDAP disabled account: pwdAccountLockedTime=000001010000Z (part of PPolicy module) + ldapUserAccountStatusInterpreter.setDisabledAccountPropertyValue("000001010000Z"); + + testLDAPDisableUserAccount(ldapUserAccountStatusInterpreter, null, "000001010000Z"); + } + /** * Tests a forced update of the test users and groups with deletions disabled. No users or groups should be deleted, * whether or not they move registry. Groups that would have been deleted should have no members and should only be @@ -785,6 +919,16 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase return person; } + private NodeDescription newPersonWithUserAccountStatusProperty(String userName, String userAccountPropertyValue) + { + NodeDescription person = newPerson(userName, userName + "@somedomain.com"); + + person.getProperties().put(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "userAccountStatusProperty"), userAccountPropertyValue); + person.setLastModified(new Date()); + + return person; + } + /** * Perform all the necessary assertions to ensure that an authority and its members exist in the correct zone. * @@ -936,10 +1080,31 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase } + public static interface IMockUserRegistry extends UserRegistry, ActivateableBean + { + /** + * Gets the zone id. + * + * @return the zoneId + */ + String getZoneId(); + + /** + * Modifies the state to match the arguments. Compares new with old and + * records new modification dates only for changes. + * + * @param persons + * the persons + * @param groups + * the groups + */ + void updateState(Collection persons, Collection groups); + } + /** * A Mock {@link UserRegistry} that returns a fixed set of users and groups. */ - public static class MockUserRegistry implements UserRegistry, ActivateableBean + public static class MockUserRegistry implements IMockUserRegistry { private boolean isActive = true; @@ -973,15 +1138,7 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase - /** - * Modifies the state to match the arguments. Compares new with old and records new modification dates only for - * changes. - * - * @param persons - * the persons - * @param groups - * the groups - */ + @Override public void updateState(Collection persons, Collection groups) { List newPersons = new ArrayList(this.persons); @@ -1066,11 +1223,7 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase this(zoneId, Arrays.asList(persons), Arrays.asList(groups)); } - /** - * Gets the zone id. - * - * @return the zoneId - */ + @Override public String getZoneId() { return this.zoneId; @@ -1195,10 +1348,10 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase * @param registries * the new user registries */ - public void setUserRegistries(MockUserRegistry... registries) + public void setUserRegistries(IMockUserRegistry... registries) { this.contexts = new LinkedHashMap(registries.length * 2); - for (MockUserRegistry registry : registries) + for (IMockUserRegistry registry : registries) { StaticApplicationContext context = new StaticApplicationContext(); context.getDefaultListableBeanFactory().registerSingleton("userRegistry", registry); @@ -1230,7 +1383,7 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase public void updateZone(String zoneId, NodeDescription[] persons, NodeDescription[] groups) { ApplicationContext context = this.contexts.get(zoneId); - MockUserRegistry registry = (MockUserRegistry) context.getBean("userRegistry"); + IMockUserRegistry registry = (IMockUserRegistry) context.getBean("userRegistry"); registry.updateState(Arrays.asList(persons), Arrays.asList(groups)); }