mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-07 17:49:17 +00:00
Merged BRANCHES/DEV/DAVEW/LDAP to HEAD
14587: Added new node service method getNodesWithoutParentAssocsOfType to public-services-security-context.xml (or at least my best guess at it!) 14586: Use US spelling of synchronization in filenames for consistency 14585: Lower the default user registry sync frequency to daily instead of hourly. Now users and groups are pulled over incrementally on login of missing users. 14583: Unit test for ChainingUserRegistrySynchronizer 14571: Migration patch for existing authorities previously held in users store - Uses AuthorityService to recreate authorities in spaces store with new structure 14555: Authority service changes for LDAP sync improvements - Moved sys:authorities container to spaces store - All authorities now stored directly under sys:authorities - Authorities can now be looked up directly by node service - Secondary child associations used to model group relationships - 'Root' groups for UI navigation determined dynamically by node service query - cm:member association used to relate both authority containers and persons to other authorities - New cm:inZone association relates persons and authority containers to synchronization 'zones' stored under sys:zones - Look up of authority zone and all authorities in a zone to enable multi-zone LDAP sync 14524: Dev branch for finishing LDAP zones and upgrade impact git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@14588 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2009 Alfresco Software Limited.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
|
||||
* This program 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 General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
* As a special exception to the terms and conditions of version 2.0 of
|
||||
* the GPL, you may redistribute this Program in connection with Free/Libre
|
||||
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
||||
* FLOSS exception. You should have received a copy of the text describing
|
||||
* the FLOSS exception, and it is also available here:
|
||||
* http://www.alfresco.com/legal/licensing"
|
||||
*/
|
||||
package org.alfresco.repo.security.sync;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.attributes.Attribute;
|
||||
import org.alfresco.repo.attributes.LongAttributeValue;
|
||||
import org.alfresco.repo.attributes.MapAttributeValue;
|
||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||
import org.alfresco.repo.management.subsystems.ChildApplicationContextManager;
|
||||
import org.alfresco.service.cmr.attributes.AttributeService;
|
||||
import org.alfresco.service.cmr.security.AuthorityService;
|
||||
import org.alfresco.service.cmr.security.AuthorityType;
|
||||
import org.alfresco.service.cmr.security.PersonService;
|
||||
import org.alfresco.util.PropertyMap;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
* A <code>ChainingUserRegistrySynchronizer</code> is responsible for synchronizing Alfresco's local user (person) and
|
||||
* group (authority) information with the external subsystems in the authentication chain (most typically LDAP
|
||||
* directories). When the {@link #synchronize(boolean)} method is called, it visits each {@link UserRegistry} bean in
|
||||
* the 'chain' of application contexts, managed by a {@link ChildApplicationContextManager}, and compares its
|
||||
* timestamped user and group information with the local users and groups last retrieved from the same source. Any
|
||||
* updates and additions made to those users and groups are applied to the local copies. The ordering of each
|
||||
* {@link UserRegistry} in the chain determines its precedence when it comes to user and group name collisions.
|
||||
* <p>
|
||||
* The <code>force</code> argument determines whether a complete or partial set of information is queried from the
|
||||
* {@link UserRegistry}. When <code>true</code> then <i>all</i> users and groups are queried. With this complete set of
|
||||
* information, the synchronizer is able to identify which users and groups have been deleted, so it will delete users
|
||||
* and groups as well as update and create them. Since processing all users and groups may be fairly time consuming, it
|
||||
* is recommended this mode is only used by a background scheduled synchronization job. When the argument is
|
||||
* <code>false</code> then only those users and groups modified since the most recent modification date of all the
|
||||
* objects last queried from the same {@link UserRegistry} are retrieved. In this mode, local users and groups are
|
||||
* created and updated, but not deleted (except where a name collision with a lower priority {@link UserRegistry} is
|
||||
* detected). This 'differential' mode is much faster, and by default is triggered when a user is successfully
|
||||
* authenticated who doesn't yet have a local person object in Alfresco. This should mean that new users and their group
|
||||
* information are pulled over from LDAP servers as and when required.
|
||||
*
|
||||
* @author dward
|
||||
*/
|
||||
public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronizer
|
||||
{
|
||||
/** The logger. */
|
||||
private static final Log logger = LogFactory.getLog(ChainingUserRegistrySynchronizer.class);
|
||||
|
||||
/** The path in the attribute service below which we persist attributes. */
|
||||
private static final String ROOT_ATTRIBUTE_PATH = ".ChainingUserRegistrySynchronizer";
|
||||
|
||||
/** The label under which the last group modification timestamp is stored for each zone. */
|
||||
private static final String GROUP_LAST_MODIFIED_ATTRIBUTE = "GROUP";
|
||||
|
||||
/** The label under which the last user modification timestamp is stored for each zone. */
|
||||
private static final String PERSON_LAST_MODIFIED_ATTRIBUTE = "PERSON";
|
||||
|
||||
/** The manager for the autentication chain to be traversed. */
|
||||
private ChildApplicationContextManager applicationContextManager;
|
||||
|
||||
/** The name used to look up a {@link UserRegistry} bean in each child application context. */
|
||||
private String sourceBeanName;
|
||||
|
||||
/** The authority service. */
|
||||
private AuthorityService authorityService;
|
||||
|
||||
/** The person service. */
|
||||
private PersonService personService;
|
||||
|
||||
/** The attribute service. */
|
||||
private AttributeService attributeService;
|
||||
|
||||
/**
|
||||
* Sets the application context manager.
|
||||
*
|
||||
* @param applicationContextManager
|
||||
* the applicationContextManager to set
|
||||
*/
|
||||
public void setApplicationContextManager(ChildApplicationContextManager applicationContextManager)
|
||||
{
|
||||
this.applicationContextManager = applicationContextManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name used to look up a {@link UserRegistry} bean in each child application context.
|
||||
*
|
||||
* @param sourceBeanName
|
||||
* the bean name
|
||||
*/
|
||||
public void setSourceBeanName(String sourceBeanName)
|
||||
{
|
||||
this.sourceBeanName = sourceBeanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authority service.
|
||||
*
|
||||
* @param authorityService
|
||||
* the new authority service
|
||||
*/
|
||||
public void setAuthorityService(AuthorityService authorityService)
|
||||
{
|
||||
this.authorityService = authorityService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the person service.
|
||||
*
|
||||
* @param personService
|
||||
* the new person service
|
||||
*/
|
||||
public void setPersonService(PersonService personService)
|
||||
{
|
||||
this.personService = personService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attribute service.
|
||||
*
|
||||
* @param attributeService
|
||||
* the new attribute service
|
||||
*/
|
||||
public void setAttributeService(AttributeService attributeService)
|
||||
{
|
||||
this.attributeService = attributeService;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#synchronize(boolean)
|
||||
*/
|
||||
public void synchronize(boolean force)
|
||||
{
|
||||
Set<String> visitedZoneIds = new TreeSet<String>();
|
||||
for (String zoneId : this.applicationContextManager.getInstanceIds())
|
||||
{
|
||||
ApplicationContext context = this.applicationContextManager.getApplicationContext(zoneId);
|
||||
try
|
||||
{
|
||||
UserRegistry plugin = (UserRegistry) context.getBean(this.sourceBeanName);
|
||||
if (!(plugin instanceof ActivateableBean) || ((ActivateableBean) plugin).isActive())
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.info("Synchronizing users and groups with user registry '"
|
||||
+ zoneId + "'");
|
||||
if (force)
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger
|
||||
.warn("Forced synchronization with user registry '"
|
||||
+ zoneId
|
||||
+ "'; some users and groups previously created by synchronization with this user registry may be removed.");
|
||||
}
|
||||
int personsProcessed = syncPersonsWithPlugin(zoneId, plugin, force, visitedZoneIds);
|
||||
int groupsProcessed = syncGroupsWithPlugin(zoneId, plugin, force, visitedZoneIds);
|
||||
ChainingUserRegistrySynchronizer.logger
|
||||
.info("Finished synchronizing users and groups with user registry '" + zoneId + "'");
|
||||
logger.info(personsProcessed + " user(s) and " + groupsProcessed + " group(s) processed");
|
||||
}
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException e)
|
||||
{
|
||||
// Ignore and continue
|
||||
}
|
||||
visitedZoneIds.add(zoneId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes local users (persons) with a {@link UserRegistry} for a particular zone.
|
||||
*
|
||||
* @param zoneId
|
||||
* the zone id. This identifier is used to tag all created users, so that in the future we can tell those
|
||||
* that have been deleted from the registry.
|
||||
* @param userRegistry
|
||||
* the user registry for the zone.
|
||||
* @param force
|
||||
* <code>true</code> if all persons are to be queried. <code>false</code> if only those changed since the
|
||||
* most recent queried user should be queried.
|
||||
* @param visitedZoneIds
|
||||
* the set of zone ids already processed. These zones have precedence over the current zone when it comes
|
||||
* to user name 'collisions'. If a user is queried that already exists locally but is tagged with one of
|
||||
* the zones in this set, then it will be ignored as this zone has lower priority.
|
||||
* @return the number of users processed
|
||||
*/
|
||||
private int syncPersonsWithPlugin(String zoneId, UserRegistry userRegistry, boolean force,
|
||||
Set<String> visitedZoneIds)
|
||||
{
|
||||
int processedCount = 0;
|
||||
long lastModifiedMillis = force ? -1L : getMostRecentUpdateTime(
|
||||
ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId);
|
||||
Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
|
||||
if (lastModified == null)
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.info("Retrieving all users from user registry '" + zoneId + "'");
|
||||
}
|
||||
else
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.info("Retrieving users changed since "
|
||||
+ DateFormat.getDateTimeInstance().format(lastModified) + " from user registry '" + zoneId + "'");
|
||||
}
|
||||
Iterator<NodeDescription> persons = userRegistry.getPersons(lastModified);
|
||||
Set<String> personsToDelete = this.authorityService.getAllAuthoritiesInZone(zoneId, AuthorityType.USER);
|
||||
while (persons.hasNext())
|
||||
{
|
||||
NodeDescription person = persons.next();
|
||||
PropertyMap personProperties = person.getProperties();
|
||||
String personName = (String) personProperties.get(ContentModel.PROP_USERNAME);
|
||||
if (personsToDelete.remove(personName))
|
||||
{
|
||||
// The person already existed in this zone: update the person
|
||||
ChainingUserRegistrySynchronizer.logger.info("Updating user '" + personName + "'");
|
||||
this.personService.setPersonProperties(personName, personProperties);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The person does not exist in this zone, but may exist in another zone
|
||||
String zone = this.authorityService.getAuthorityZone(personName);
|
||||
if (zone != null)
|
||||
{
|
||||
if (visitedZoneIds.contains(zone))
|
||||
{
|
||||
// A person that exists in a different zone with higher precedence
|
||||
continue;
|
||||
}
|
||||
// The person existed, but in a zone with lower precedence
|
||||
ChainingUserRegistrySynchronizer.logger
|
||||
.warn("Recreating occluded user '"
|
||||
+ personName
|
||||
+ "'. This user was previously created manually or through synchronization with a lower priority user registry.");
|
||||
this.personService.deletePerson(personName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The person did not exist at all
|
||||
ChainingUserRegistrySynchronizer.logger.info("Creating user '" + personName + "'");
|
||||
}
|
||||
this.personService.createPerson(personProperties, zoneId);
|
||||
}
|
||||
// Increment the count of processed people
|
||||
processedCount++;
|
||||
|
||||
// Maintain the last modified date
|
||||
Date personLastModified = person.getLastModified();
|
||||
if (personLastModified != null)
|
||||
{
|
||||
lastModifiedMillis = Math.max(lastModifiedMillis, personLastModified.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
if (force && !personsToDelete.isEmpty())
|
||||
{
|
||||
for (String personName : personsToDelete)
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.warn("Deleting user '" + personName + "'");
|
||||
this.personService.deletePerson(personName);
|
||||
processedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModifiedMillis != -1)
|
||||
{
|
||||
setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
|
||||
lastModifiedMillis);
|
||||
}
|
||||
|
||||
return processedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes local groups (authorities) with a {@link UserRegistry} for a particular zone.
|
||||
*
|
||||
* @param zoneId
|
||||
* the zone id. This identifier is used to tag all created groups, so that in the future we can tell
|
||||
* those that have been deleted from the registry.
|
||||
* @param userRegistry
|
||||
* the user registry for the zone.
|
||||
* @param force
|
||||
* <code>true</code> if all groups are to be queried. <code>false</code> if only those changed since the
|
||||
* most recent queried group should be queried.
|
||||
* @param visitedZoneIds
|
||||
* the set of zone ids already processed. These zones have precedence over the current zone when it comes
|
||||
* to group name 'collisions'. If a group is queried that already exists locally but is tagged with one
|
||||
* of the zones in this set, then it will be ignored as this zone has lower priority.
|
||||
* @return the number of groups processed
|
||||
*/
|
||||
private int syncGroupsWithPlugin(String zoneId, UserRegistry plugin, boolean force, Set<String> visitedZoneIds)
|
||||
{
|
||||
int processedCount = 0;
|
||||
long lastModifiedMillis = force ? -1L : getMostRecentUpdateTime(
|
||||
ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId);
|
||||
Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
|
||||
if (lastModified == null)
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.info("Retrieving all groups from user registry '" + zoneId + "'");
|
||||
}
|
||||
else
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.info("Retrieving groups changed since "
|
||||
+ DateFormat.getDateTimeInstance().format(lastModified) + " from user registry '" + zoneId + "'");
|
||||
}
|
||||
|
||||
Iterator<NodeDescription> groups = plugin.getGroups(lastModified);
|
||||
Map<String, Set<String>> groupAssocsToCreate = new TreeMap<String, Set<String>>();
|
||||
Set<String> groupsToDelete = this.authorityService.getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP);
|
||||
while (groups.hasNext())
|
||||
{
|
||||
NodeDescription group = groups.next();
|
||||
PropertyMap groupProperties = group.getProperties();
|
||||
String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
|
||||
if (groupsToDelete.remove(groupName))
|
||||
{
|
||||
// update an existing group in the same zone
|
||||
Set<String> oldChildren = this.authorityService.getContainedAuthorities(null, groupName, true);
|
||||
Set<String> newChildren = group.getChildAssociations();
|
||||
Set<String> toDelete = new TreeSet<String>(oldChildren);
|
||||
Set<String> toAdd = new TreeSet<String>(newChildren);
|
||||
toDelete.removeAll(newChildren);
|
||||
toAdd.removeAll(oldChildren);
|
||||
if (!toAdd.isEmpty())
|
||||
{
|
||||
groupAssocsToCreate.put(groupName, toAdd);
|
||||
}
|
||||
for (String child : toDelete)
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.info("Removing '"
|
||||
+ this.authorityService.getShortName(child) + "' from group '"
|
||||
+ this.authorityService.getShortName(groupName) + "'");
|
||||
this.authorityService.removeAuthority(groupName, child);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String groupShortName = this.authorityService.getShortName(groupName);
|
||||
String groupZone = this.authorityService.getAuthorityZone(groupName);
|
||||
if (groupZone != null)
|
||||
{
|
||||
if (visitedZoneIds.contains(groupZone))
|
||||
{
|
||||
// A group that exists in a different zone with higher precedence
|
||||
continue;
|
||||
}
|
||||
// The group existed, but in a zone with lower precedence
|
||||
ChainingUserRegistrySynchronizer.logger
|
||||
.warn("Recreating occluded group '"
|
||||
+ groupShortName
|
||||
+ "'. This group was previously created manually or through synchronization with a lower priority user registry.");
|
||||
this.authorityService.deleteAuthority(groupName);
|
||||
}
|
||||
else
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.info("Creating group '" + groupShortName + "'");
|
||||
}
|
||||
|
||||
// create the group
|
||||
this.authorityService.createAuthority(AuthorityType.getAuthorityType(groupName), groupShortName,
|
||||
(String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME), zoneId);
|
||||
Set<String> children = group.getChildAssociations();
|
||||
if (!children.isEmpty())
|
||||
{
|
||||
groupAssocsToCreate.put(groupName, children);
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the count of processed groups
|
||||
processedCount++;
|
||||
|
||||
// Maintain the last modified date
|
||||
Date groupLastModified = group.getLastModified();
|
||||
if (groupLastModified != null)
|
||||
{
|
||||
lastModifiedMillis = Math.max(lastModifiedMillis, groupLastModified.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new associations, now that we have created everything
|
||||
for (Map.Entry<String, Set<String>> entry : groupAssocsToCreate.entrySet())
|
||||
{
|
||||
for (String child : entry.getValue())
|
||||
{
|
||||
String groupName = entry.getKey();
|
||||
ChainingUserRegistrySynchronizer.logger.info("Adding '" + this.authorityService.getShortName(child)
|
||||
+ "' to group '" + this.authorityService.getShortName(groupName) + "'");
|
||||
this.authorityService.addAuthority(groupName, child);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Delete groups if we have complete information for the zone
|
||||
if (force && !groupsToDelete.isEmpty())
|
||||
{
|
||||
for (String group : groupsToDelete)
|
||||
{
|
||||
ChainingUserRegistrySynchronizer.logger.warn("Deleting group '"
|
||||
+ this.authorityService.getShortName(group) + "'");
|
||||
this.authorityService.deleteAuthority(group);
|
||||
processedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModifiedMillis != -1)
|
||||
{
|
||||
setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
|
||||
lastModifiedMillis);
|
||||
}
|
||||
|
||||
return processedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the persisted most recent update time for a label and zone.
|
||||
*
|
||||
* @param label
|
||||
* the label
|
||||
* @param zoneId
|
||||
* the zone id
|
||||
* @return the most recent update time in milliseconds
|
||||
*/
|
||||
private long getMostRecentUpdateTime(String label, String zoneId)
|
||||
{
|
||||
Attribute attribute = this.attributeService.getAttribute(ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH
|
||||
+ '/' + label + '/' + zoneId);
|
||||
return attribute == null ? -1 : attribute.getLongValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the most recent update time for a label and zone.
|
||||
*
|
||||
* @param label
|
||||
* the label
|
||||
* @param zoneId
|
||||
* the zone id
|
||||
* @param lastModifiedMillis
|
||||
* the update time in milliseconds
|
||||
*/
|
||||
private void setMostRecentUpdateTime(String label, String zoneId, long lastModifiedMillis)
|
||||
{
|
||||
String path = ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH + '/' + label;
|
||||
if (!this.attributeService.exists(path))
|
||||
{
|
||||
if (!this.attributeService.exists(ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH))
|
||||
{
|
||||
this.attributeService.setAttribute("", ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH,
|
||||
new MapAttributeValue());
|
||||
}
|
||||
this.attributeService.setAttribute(ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, label,
|
||||
new MapAttributeValue());
|
||||
}
|
||||
this.attributeService.setAttribute(path, zoneId, new LongAttributeValue(lastModifiedMillis));
|
||||
}
|
||||
}
|
@@ -0,0 +1,524 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2009 Alfresco Software Limited.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
|
||||
* This program 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 General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
* As a special exception to the terms and conditions of version 2.0 of
|
||||
* the GPL, you may redistribute this Program in connection with Free/Libre
|
||||
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
||||
* FLOSS exception. You should have received a copy of the text describing
|
||||
* the FLOSS exception, and it is also available here:
|
||||
* http://www.alfresco.com/legal/licensing"
|
||||
*/
|
||||
package org.alfresco.repo.security.sync;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.management.subsystems.ChildApplicationContextManager;
|
||||
import org.alfresco.service.cmr.repository.NodeRef;
|
||||
import org.alfresco.service.cmr.repository.NodeService;
|
||||
import org.alfresco.service.cmr.security.AuthorityService;
|
||||
import org.alfresco.service.cmr.security.AuthorityType;
|
||||
import org.alfresco.service.cmr.security.PersonService;
|
||||
import org.alfresco.util.BaseSpringTest;
|
||||
import org.alfresco.util.PropertyMap;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.engine.SessionFactoryImplementor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
|
||||
/**
|
||||
* Tests the {@link ChainingUserRegistrySynchronizer} using a simulated {@link UserRegistry}.
|
||||
*
|
||||
* @author dward
|
||||
*/
|
||||
public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest
|
||||
{
|
||||
|
||||
/** The context locations, in reverse priority order. */
|
||||
private static final String[] CONFIG_LOCATIONS =
|
||||
{
|
||||
"classpath:alfresco/application-context.xml", "classpath:sync-test-context.xml"
|
||||
};
|
||||
|
||||
/** The synchronizer we are testing. */
|
||||
private UserRegistrySynchronizer synchronizer;
|
||||
|
||||
/** The application context manager. */
|
||||
private MockApplicationContextManager applicationContextManager;
|
||||
|
||||
/** The person service. */
|
||||
private PersonService personService;
|
||||
|
||||
/** The authority service. */
|
||||
private AuthorityService authorityService;
|
||||
|
||||
/** The node service. */
|
||||
private NodeService nodeService;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.test.AbstractTransactionalSpringContextTests#onSetUpInTransaction()
|
||||
*/
|
||||
@Override
|
||||
protected void onSetUpInTransaction() throws Exception
|
||||
{
|
||||
ApplicationContext context = getApplicationContext();
|
||||
this.synchronizer = (UserRegistrySynchronizer) context.getBean("testUserRegistrySynchronizer");
|
||||
this.applicationContextManager = (MockApplicationContextManager) context
|
||||
.getBean("testApplicationContextManager");
|
||||
this.personService = (PersonService) context.getBean("personService");
|
||||
this.authorityService = (AuthorityService) context.getBean("authorityService");
|
||||
this.nodeService = (NodeService) context.getBean("nodeService");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.test.AbstractTransactionalSpringContextTests#onTearDownInTransaction()
|
||||
*/
|
||||
protected void onTearDownInTransaction() throws Exception
|
||||
{
|
||||
flushAndClear();
|
||||
|
||||
// Try to clear the Hibernate L2 cache so we have consistency after a rollback
|
||||
SessionFactory sessionFactory = getSession().getSessionFactory();
|
||||
String[] persistentClasses = ((SessionFactoryImplementor) sessionFactory).getImplementors("java.lang.Object");
|
||||
for (String persistentClass : persistentClasses)
|
||||
{
|
||||
sessionFactory.evictEntity(persistentClass);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.util.BaseSpringTest#getConfigLocations()
|
||||
*/
|
||||
@Override
|
||||
protected String[] getConfigLocations()
|
||||
{
|
||||
return ChainingUserRegistrySynchronizerTest.CONFIG_LOCATIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the test users and groups in two zones, "Z1" and "Z2", by doing a forced synchronize with a Mock user
|
||||
* registry. Note that the zones have some overlapping entries. The layout is as follows
|
||||
*
|
||||
* <pre>
|
||||
* Z1
|
||||
* G1
|
||||
* G2 - U1, G3 - U2, G4, G5
|
||||
*
|
||||
* Z2
|
||||
* G2 - U1, U3, U4
|
||||
* G6 - U3, U4, G7 - U5
|
||||
* </pre>
|
||||
*
|
||||
* @throws Exception
|
||||
* the exception
|
||||
*/
|
||||
public void setUpTestUsersAndGroups() throws Exception
|
||||
{
|
||||
this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[]
|
||||
{
|
||||
newPerson("U1"), newPerson("U2")
|
||||
}, new NodeDescription[]
|
||||
{
|
||||
newGroup("G1"), newGroup("G2", "U1", "G3"), newGroup("G3", "U2", "G4", "G5"), newGroup("G4"),
|
||||
newGroup("G5")
|
||||
}), new MockUserRegistry("Z2", new NodeDescription[]
|
||||
{
|
||||
newPerson("U1"), newPerson("U3"), newPerson("U4"), newPerson("U5")
|
||||
}, new NodeDescription[]
|
||||
{
|
||||
newGroup("G2", "U1", "U3", "U4"), newGroup("G6", "U3", "U4", "G7"), newGroup("G7", "U5")
|
||||
}));
|
||||
this.synchronizer.synchronize(true);
|
||||
assertExists("Z1", "U1");
|
||||
assertExists("Z1", "U2");
|
||||
assertExists("Z1", "G1");
|
||||
assertExists("Z1", "G2", "U1", "G3");
|
||||
assertExists("Z1", "G3", "U2", "G4", "G5");
|
||||
assertExists("Z1", "G4");
|
||||
assertExists("Z1", "G5");
|
||||
assertExists("Z2", "U3");
|
||||
assertExists("Z2", "U4");
|
||||
assertExists("Z2", "U5");
|
||||
assertExists("Z2", "G6", "U3", "U4", "G7");
|
||||
assertExists("Z2", "G7", "U5");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a differential update of the test users and groups. The layout is as follows
|
||||
*
|
||||
* <pre>
|
||||
* Z1
|
||||
* G1 - U1, U6
|
||||
* G2 - U1
|
||||
* G3 - U2, G4, G5 - U6
|
||||
*
|
||||
* Z2
|
||||
* G2 - U1, U3, U4, U6
|
||||
* G6 - U3, U4, G7
|
||||
* </pre>
|
||||
*
|
||||
* @throws Exception
|
||||
* the exception
|
||||
*/
|
||||
public void testDifferentialUpdate() throws Exception
|
||||
{
|
||||
setUpTestUsersAndGroups();
|
||||
this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[]
|
||||
{
|
||||
newPerson("U1", "changeofemail@alfresco.com"), newPerson("U6")
|
||||
}, new NodeDescription[]
|
||||
{
|
||||
newGroup("G1", "U1", "U6"), newGroup("G2", "U1"), newGroup("G5", "U6")
|
||||
}), new MockUserRegistry("Z2", new NodeDescription[]
|
||||
{
|
||||
newPerson("U1", "shouldbeignored@alfresco.com"), newPerson("U5", "u5email@alfresco.com"), newPerson("U6")
|
||||
}, new NodeDescription[]
|
||||
{
|
||||
newGroup("G2", "U1", "U3", "U4", "U6"), newGroup("G7")
|
||||
}));
|
||||
this.synchronizer.synchronize(false);
|
||||
assertExists("Z1", "U1");
|
||||
assertEmailEquals("U1", "changeofemail@alfresco.com");
|
||||
assertExists("Z1", "U2");
|
||||
assertExists("Z1", "U6");
|
||||
assertExists("Z1", "G1", "U1", "U6");
|
||||
assertExists("Z1", "G2", "U1");
|
||||
assertExists("Z1", "G3", "U2", "G4", "G5");
|
||||
assertExists("Z1", "G4");
|
||||
assertExists("Z1", "G5", "U6");
|
||||
assertExists("Z2", "U3");
|
||||
assertExists("Z2", "U4");
|
||||
assertExists("Z2", "U5");
|
||||
assertEmailEquals("U5", "u5email@alfresco.com");
|
||||
assertExists("Z2", "G6", "U3", "U4", "G7");
|
||||
assertExists("Z2", "G7");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a forced update of the test users and groups. Also tests that groups and users that previously existed in
|
||||
* Z2 get moved when they appear in Z1. The layout is as follows
|
||||
*
|
||||
* <pre>
|
||||
* Z1
|
||||
* G1 - U6
|
||||
* G2 -
|
||||
* G3 - U2, G5 - U6
|
||||
* G6 - U3
|
||||
*
|
||||
* Z2
|
||||
* G2 - U1, U3, U6
|
||||
* G6 - U3, G7
|
||||
* </pre>
|
||||
*
|
||||
* @throws Exception
|
||||
* the exception
|
||||
*/
|
||||
public void testForcedUpdate() throws Exception
|
||||
{
|
||||
setUpTestUsersAndGroups();
|
||||
this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[]
|
||||
{
|
||||
newPerson("U2"), newPerson("U3"), newPerson("U6")
|
||||
}, new NodeDescription[]
|
||||
{
|
||||
newGroup("G1", "U6"), newGroup("G2"), newGroup("G3", "U2", "G5"), newGroup("G5", "U6"),
|
||||
newGroup("G6", "U3")
|
||||
}), new MockUserRegistry("Z2", new NodeDescription[]
|
||||
{
|
||||
newPerson("U1", "somenewemail@alfresco.com"), newPerson("U3"), newPerson("U6")
|
||||
}, new NodeDescription[]
|
||||
{
|
||||
newGroup("G2", "U1", "U3", "U6"), newGroup("G6", "U3", "G7"), newGroup("G7")
|
||||
}));
|
||||
this.synchronizer.synchronize(true);
|
||||
assertExists("Z1", "U2");
|
||||
assertExists("Z1", "U3");
|
||||
assertExists("Z1", "U6");
|
||||
assertExists("Z1", "G1", "U6");
|
||||
assertExists("Z1", "G2");
|
||||
assertExists("Z1", "G3", "U2", "G5");
|
||||
assertNotExists("G4");
|
||||
assertExists("Z1", "G5", "U6");
|
||||
assertExists("Z1", "G6", "U3");
|
||||
assertExists("Z2", "U1");
|
||||
assertEmailEquals("U1", "somenewemail@alfresco.com");
|
||||
assertNotExists("U4");
|
||||
assertNotExists("U5");
|
||||
assertExists("Z2", "G7");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a description of a test group
|
||||
*
|
||||
* @param name
|
||||
* the name
|
||||
* @param members
|
||||
* the members
|
||||
* @return the node description
|
||||
*/
|
||||
private NodeDescription newGroup(String name, String... members)
|
||||
{
|
||||
NodeDescription group = new NodeDescription();
|
||||
PropertyMap properties = group.getProperties();
|
||||
properties.put(ContentModel.PROP_AUTHORITY_NAME, longName(name));
|
||||
if (members.length > 0)
|
||||
{
|
||||
Set<String> assocs = group.getChildAssociations();
|
||||
for (String member : members)
|
||||
{
|
||||
assocs.add(longName(member));
|
||||
}
|
||||
}
|
||||
group.setLastModified(new Date());
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a description of a test person with default email (userName@alfresco.com)
|
||||
*
|
||||
* @param userName
|
||||
* the user name
|
||||
* @return the node description
|
||||
*/
|
||||
private NodeDescription newPerson(String userName)
|
||||
{
|
||||
return newPerson(userName, userName + "@alfresco.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a description of a test person with a given email.
|
||||
*
|
||||
* @param userName
|
||||
* the user name
|
||||
* @param email
|
||||
* the email
|
||||
* @return the node description
|
||||
*/
|
||||
private NodeDescription newPerson(String userName, String email)
|
||||
{
|
||||
NodeDescription person = new NodeDescription();
|
||||
PropertyMap properties = person.getProperties();
|
||||
properties.put(ContentModel.PROP_USERNAME, userName);
|
||||
properties.put(ContentModel.PROP_FIRSTNAME, userName + "F");
|
||||
properties.put(ContentModel.PROP_LASTNAME, userName + "L");
|
||||
properties.put(ContentModel.PROP_EMAIL, email);
|
||||
person.setLastModified(new Date());
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform all the necessary assertions to ensure that an authority and its members exist in the correct zone.
|
||||
*
|
||||
* @param zone
|
||||
* the zone
|
||||
* @param name
|
||||
* the name
|
||||
* @param members
|
||||
* the members
|
||||
*/
|
||||
private void assertExists(String zone, String name, String... members)
|
||||
{
|
||||
String longName = longName(name);
|
||||
// Check authority exists
|
||||
assertTrue(this.authorityService.authorityExists(longName));
|
||||
|
||||
// Check in correct zone
|
||||
assertEquals(zone, this.authorityService.getAuthorityZone(longName));
|
||||
if (AuthorityType.getAuthorityType(longName).equals(AuthorityType.GROUP))
|
||||
{
|
||||
// Check groups have expected members
|
||||
Set<String> memberSet = new HashSet<String>(members.length * 2);
|
||||
for (String member : members)
|
||||
{
|
||||
memberSet.add(longName(member));
|
||||
}
|
||||
assertEquals(memberSet, this.authorityService.getContainedAuthorities(null, longName, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check users exist as persons
|
||||
assertTrue(this.personService.personExists(name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform all the necessary assertions to ensure that an authority does not exist.
|
||||
*
|
||||
* @param name
|
||||
* the name
|
||||
*/
|
||||
private void assertNotExists(String name)
|
||||
{
|
||||
String longName = longName(name);
|
||||
// Check authority does not exist
|
||||
assertFalse(this.authorityService.authorityExists(longName));
|
||||
|
||||
// Check there is no zone
|
||||
assertNull(this.authorityService.getAuthorityZone(longName));
|
||||
if (!AuthorityType.getAuthorityType(longName).equals(AuthorityType.GROUP))
|
||||
{
|
||||
// Check person does not exist
|
||||
assertFalse(this.personService.personExists(name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a person's email has the expected value
|
||||
*
|
||||
* @param personName
|
||||
* the person name
|
||||
* @param email
|
||||
* the email
|
||||
*/
|
||||
private void assertEmailEquals(String personName, String email)
|
||||
{
|
||||
NodeRef personRef = this.personService.getPerson(personName);
|
||||
assertEquals(email, this.nodeService.getProperty(personRef, ContentModel.PROP_EMAIL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given short name to a full authority name, assuming that those short names beginning with 'G'
|
||||
* correspond to groups and all others correspond to users.
|
||||
*
|
||||
* @param shortName
|
||||
* the short name
|
||||
* @return the full authority name
|
||||
*/
|
||||
private String longName(String shortName)
|
||||
{
|
||||
return this.authorityService.getName(shortName.startsWith("G") ? AuthorityType.GROUP : AuthorityType.USER,
|
||||
shortName);
|
||||
}
|
||||
|
||||
/**
|
||||
* A Mock {@link UserRegistry} that returns a fixed set of users and groups.
|
||||
*/
|
||||
public static class MockUserRegistry implements UserRegistry
|
||||
{
|
||||
|
||||
/** The zone id. */
|
||||
private String zoneId;
|
||||
|
||||
/** The persons. */
|
||||
private NodeDescription[] persons;
|
||||
|
||||
/** The groups. */
|
||||
private NodeDescription[] groups;
|
||||
|
||||
/**
|
||||
* Instantiates a new mock user registry.
|
||||
*
|
||||
* @param zoneId
|
||||
* the zone id
|
||||
* @param persons
|
||||
* the persons
|
||||
* @param groups
|
||||
* the groups
|
||||
*/
|
||||
public MockUserRegistry(String zoneId, NodeDescription[] persons, NodeDescription[] groups)
|
||||
{
|
||||
this.zoneId = zoneId;
|
||||
this.persons = persons;
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the zone id.
|
||||
*
|
||||
* @return the zoneId
|
||||
*/
|
||||
public String getZoneId()
|
||||
{
|
||||
return this.zoneId;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.repo.security.sync.UserRegistry#getGroups(java.util.Date)
|
||||
*/
|
||||
public Iterator<NodeDescription> getGroups(Date modifiedSince)
|
||||
{
|
||||
return Arrays.asList(this.groups).iterator();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.repo.security.sync.UserRegistry#getPersons(java.util.Date)
|
||||
*/
|
||||
public Iterator<NodeDescription> getPersons(Date modifiedSince)
|
||||
{
|
||||
return Arrays.asList(this.persons).iterator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link ChildApplicationContextManager} for a chain of application contexts containing mock user registries.
|
||||
*/
|
||||
public static class MockApplicationContextManager implements ChildApplicationContextManager
|
||||
{
|
||||
|
||||
/** The contexts. */
|
||||
private Map<String, ApplicationContext> contexts;
|
||||
|
||||
/**
|
||||
* Sets the user registries.
|
||||
*
|
||||
* @param registries
|
||||
* the new user registries
|
||||
*/
|
||||
public void setUserRegistries(MockUserRegistry... registries)
|
||||
{
|
||||
this.contexts = new LinkedHashMap<String, ApplicationContext>(registries.length * 2);
|
||||
for (MockUserRegistry registry : registries)
|
||||
{
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
context.getDefaultListableBeanFactory().registerSingleton("userRegistry", registry);
|
||||
this.contexts.put(registry.getZoneId(), context);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see
|
||||
* org.alfresco.repo.management.subsystems.ChildApplicationContextManager#getApplicationContext(java.lang.String
|
||||
* )
|
||||
*/
|
||||
public ApplicationContext getApplicationContext(String id)
|
||||
{
|
||||
return this.contexts.get(id);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.repo.management.subsystems.ChildApplicationContextManager#getInstanceIds()
|
||||
*/
|
||||
public Collection<String> getInstanceIds()
|
||||
{
|
||||
return this.contexts.keySet();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2009 Alfresco Software Limited.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
|
||||
* This program 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 General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
* As a special exception to the terms and conditions of version 2.0 of
|
||||
* the GPL, you may redistribute this Program in connection with Free/Libre
|
||||
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
||||
* FLOSS exception. You should have received a copy of the text describing
|
||||
* the FLOSS exception, and it is also available here:
|
||||
* http://www.alfresco.com/legal/licensing"
|
||||
*/
|
||||
package org.alfresco.repo.security.sync;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.alfresco.util.PropertyMap;
|
||||
|
||||
/**
|
||||
* An 'off-line' description of an Alfresco node.
|
||||
*
|
||||
* @author dward
|
||||
*/
|
||||
public class NodeDescription
|
||||
{
|
||||
/** The properties. */
|
||||
private final PropertyMap properties = new PropertyMap(19);
|
||||
|
||||
/** The child associations. */
|
||||
private final Set<String> childAssociations = new TreeSet<String>();
|
||||
|
||||
/** The last modification date. */
|
||||
private Date lastModified;
|
||||
|
||||
/**
|
||||
* Gets the last modification date.
|
||||
*
|
||||
* @return the last modification date
|
||||
*/
|
||||
public Date getLastModified()
|
||||
{
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last modification date.
|
||||
*
|
||||
* @param lastModified
|
||||
* the last modification date
|
||||
*/
|
||||
public void setLastModified(Date lastModified)
|
||||
{
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the properties.
|
||||
*
|
||||
* @return the properties
|
||||
*/
|
||||
public PropertyMap getProperties()
|
||||
{
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the child associations.
|
||||
*
|
||||
* @return the child associations
|
||||
*/
|
||||
public Set<String> getChildAssociations()
|
||||
{
|
||||
return childAssociations;
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2009 Alfresco Software Limited.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
|
||||
* This program 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 General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
* As a special exception to the terms and conditions of version 2.0 of
|
||||
* the GPL, you may redistribute this Program in connection with Free/Libre
|
||||
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
||||
* FLOSS exception. You should have received a copy of the text describing
|
||||
* the FLOSS exception, and it is also available here:
|
||||
* http://www.alfresco.com/legal/licensing"
|
||||
*/
|
||||
package org.alfresco.repo.security.sync;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* A <code>UserRegistry</code> is an encapsulation of an external registry from which user and group information can be
|
||||
* queried (typically an LDAP directory). Implementations may optional support the ability to query only those users and
|
||||
* groups modified since a certain time.
|
||||
*
|
||||
* @author dward
|
||||
*/
|
||||
public interface UserRegistry
|
||||
{
|
||||
/**
|
||||
* Gets descriptions of all the persons (users) in the user registry or all those changed since a certain date.
|
||||
*
|
||||
* @param modifiedSince
|
||||
* if non-null, then only descriptions of users modified since this date should be returned; if
|
||||
* <code>null</code> then descriptions of all users should be returned.
|
||||
* @return a {@link Iterator} over {@link NodeDescription}s of all the persons (users) in the user registry or all
|
||||
* those changed since a certain date. The description properties should correspond to those of an Alfresco
|
||||
* person node.
|
||||
*/
|
||||
public Iterator<NodeDescription> getPersons(Date modifiedSince);
|
||||
|
||||
/**
|
||||
* Gets descriptions of all the groups in the user registry or all those changed since a certain date.
|
||||
*
|
||||
* @param modifiedSince
|
||||
* if non-null, then only descriptions of groups modified since this date should be returned; if
|
||||
* <code>null</code> then descriptions of all groups should be returned.
|
||||
* @return a {@link Iterator} over {@link NodeDescription}s of all the groups in the user registry or all those
|
||||
* changed since a certain date. The description properties should correspond to those of an Alfresco
|
||||
* authority node.
|
||||
*/
|
||||
public Iterator<NodeDescription> getGroups(Date modifiedSince);
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2009 Alfresco Software Limited.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
|
||||
* This program 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 General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
* As a special exception to the terms and conditions of version 2.0 of
|
||||
* the GPL, you may redistribute this Program in connection with Free/Libre
|
||||
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
||||
* FLOSS exception. You should have received a copy of the text describing
|
||||
* the FLOSS exception, and it is also available here:
|
||||
* http://www.alfresco.com/legal/licensing"
|
||||
*/
|
||||
package org.alfresco.repo.security.sync;
|
||||
|
||||
/**
|
||||
* A <code>UserRegistrySynchronizer</code> is responsible for synchronizing Alfresco's local user (person) and group
|
||||
* (authority) information with one or more external sources (most typically LDAP directories).
|
||||
*
|
||||
* @author dward
|
||||
*/
|
||||
public interface UserRegistrySynchronizer
|
||||
{
|
||||
/**
|
||||
* Retrieves timestamped user and group information from configured external sources and compares it with the local
|
||||
* users and groups last retrieved from the same sources. Any updates and additions made to those users and groups
|
||||
* are applied to the local Alfresco copies.
|
||||
*
|
||||
* @param force
|
||||
* Should a complete or partial set of information be queried from the external sources? When
|
||||
* <code>true</code> then <i>all</i> users and groups are queried. With this complete set of information,
|
||||
* the implementation is able to identify which users and groups have been deleted, so it will delete
|
||||
* users and groups as well as update and create them. When <code>false</code> then each source is only
|
||||
* queried for those users and groups modified since the most recent modification date of all the objects
|
||||
* last queried from that same source. In this mode, local users and groups are created and updated, but
|
||||
* not deleted.
|
||||
*/
|
||||
public void synchronize(boolean force);
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2009 Alfresco Software Limited.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
|
||||
* This program 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 General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
* As a special exception to the terms and conditions of version 2.0 of
|
||||
* the GPL, you may redistribute this Program in connection with Free/Libre
|
||||
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
||||
* FLOSS exception. You should have received a copy of the text describing
|
||||
* the FLOSS exception, and it is also available here:
|
||||
* http://www.alfresco.com/legal/licensing"
|
||||
*/
|
||||
package org.alfresco.repo.security.sync;
|
||||
|
||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
|
||||
/**
|
||||
* A scheduled job that regularly invokes a {@link UserRegistrySynchronizer}. Supports a
|
||||
* <code>synchronizeChangesOnly</code> string parameter. When <code>"true"</code> means that the
|
||||
* {@link UserRegistrySynchronizer#synchronize(boolean)} method will be called with a <code>false</code> argument rather
|
||||
* than the default <code>true</code>.
|
||||
*
|
||||
* @author dward
|
||||
*/
|
||||
public class UserRegistrySynchronizerJob implements Job
|
||||
{
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.quartz.Job#execute(org.quartz.JobExecutionContext)
|
||||
*/
|
||||
public void execute(JobExecutionContext executionContext) throws JobExecutionException
|
||||
{
|
||||
final UserRegistrySynchronizer userRegistrySynchronizer = (UserRegistrySynchronizer) executionContext
|
||||
.getJobDetail().getJobDataMap().get("userRegistrySynchronizer");
|
||||
final String synchronizeChangesOnly = (String) executionContext.getJobDetail().getJobDataMap().get(
|
||||
"synchronizeChangesOnly");
|
||||
AuthenticationUtil.runAs(new RunAsWork<Object>()
|
||||
{
|
||||
public Object doWork() throws Exception
|
||||
{
|
||||
userRegistrySynchronizer.synchronize(synchronizeChangesOnly == null
|
||||
|| !Boolean.parseBoolean(synchronizeChangesOnly));
|
||||
return null;
|
||||
}
|
||||
}, AuthenticationUtil.getSystemUserName());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,857 @@
|
||||
/*
|
||||
* Copyright (C) 2005-2009 Alfresco Software Limited.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
|
||||
* This program 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 General Public License for more details.
|
||||
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
* As a special exception to the terms and conditions of version 2.0 of
|
||||
* the GPL, you may redistribute this Program in connection with Free/Libre
|
||||
* and Open Source Software ("FLOSS") applications as described in Alfresco's
|
||||
* FLOSS exception. You should have received a copy of the text describing
|
||||
* the FLOSS exception, and it is also available here:
|
||||
* http://www.alfresco.com/legal/licensing"
|
||||
*/
|
||||
package org.alfresco.repo.security.sync.ldap;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.Attribute;
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.InitialDirContext;
|
||||
import javax.naming.directory.SearchControls;
|
||||
import javax.naming.directory.SearchResult;
|
||||
import javax.naming.ldap.LdapName;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||
import org.alfresco.repo.security.authentication.ldap.LDAPInitialDirContextFactory;
|
||||
import org.alfresco.repo.security.sync.NodeDescription;
|
||||
import org.alfresco.repo.security.sync.UserRegistry;
|
||||
import org.alfresco.service.namespace.NamespaceService;
|
||||
import org.alfresco.service.namespace.QName;
|
||||
import org.alfresco.util.PropertyMap;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* A {@link UserRegistry} implementation with the ability to query Alfresco-like descriptions of users and groups from
|
||||
* an LDAP directory, optionally restricted to those modified since a certain time.
|
||||
*
|
||||
* @author dward
|
||||
*/
|
||||
public class LDAPUserRegistry implements UserRegistry, InitializingBean, ActivateableBean
|
||||
{
|
||||
/** The logger. */
|
||||
private static Log logger = LogFactory.getLog(LDAPUserRegistry.class);
|
||||
|
||||
/** Is this bean active? I.e. should this part of the subsystem be used? */
|
||||
private boolean active = true;
|
||||
|
||||
/** The group query. */
|
||||
private String groupQuery = "(objectclass=groupOfNames)";
|
||||
|
||||
/** The group differential query. */
|
||||
private String groupDifferentialQuery = "(&(objectclass=groupOfNames)(!(modifyTimestamp<={0})))";
|
||||
|
||||
/** The person query. */
|
||||
private String personQuery = "(objectclass=inetOrgPerson)";
|
||||
|
||||
/** The person differential query. */
|
||||
private String personDifferentialQuery = "(&(objectclass=inetOrgPerson)(!(modifyTimestamp<={0})))";
|
||||
|
||||
/** The group search base. */
|
||||
private String groupSearchBase;
|
||||
|
||||
/** The user search base. */
|
||||
private String userSearchBase;
|
||||
|
||||
/** The group id attribute name. */
|
||||
private String groupIdAttributeName = "cn";
|
||||
|
||||
/** The user id attribute name. */
|
||||
private String userIdAttributeName = "uid";
|
||||
|
||||
/** The member attribute name. */
|
||||
private String memberAttributeName = "member";
|
||||
|
||||
/** The modification timestamp attribute name. */
|
||||
private String modifyTimestampAttributeName = "modifyTimestamp";
|
||||
|
||||
/** The group type. */
|
||||
private String groupType = "groupOfNames";
|
||||
|
||||
/** The person type. */
|
||||
private String personType = "inetOrgPerson";
|
||||
|
||||
/** The ldap initial context factory. */
|
||||
private LDAPInitialDirContextFactory ldapInitialContextFactory;
|
||||
|
||||
/** The attribute mapping. */
|
||||
private Map<String, String> attributeMapping;
|
||||
|
||||
/** The namespace service. */
|
||||
private NamespaceService namespaceService;
|
||||
|
||||
/** The attribute defaults. */
|
||||
private Map<String, String> attributeDefaults;
|
||||
|
||||
/** Should we error on missing group members? */
|
||||
private boolean errorOnMissingMembers;
|
||||
|
||||
/** Should we error on duplicate group IDs? */
|
||||
private boolean errorOnDuplicateGID;
|
||||
|
||||
/** Should we error on missing group IDs? */
|
||||
private boolean errorOnMissingGID = true;
|
||||
|
||||
/** Should we error on missing user IDs? */
|
||||
private boolean errorOnMissingUID = true;
|
||||
|
||||
/** An array of all LDAP attributes to be queried from users */
|
||||
private String[] userAttributeNames;
|
||||
|
||||
/** An array of all LDAP attributes to be queried from groups */
|
||||
private String[] groupAttributeNames;
|
||||
|
||||
/** The LDAP generalized time format. */
|
||||
private static DateFormat LDAP_GENERALIZED_TIME_FORMAT;
|
||||
static
|
||||
{
|
||||
LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
|
||||
LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this bean is active. I.e. should this part of the subsystem be used?
|
||||
*
|
||||
* @param active
|
||||
* <code>true</code> if this bean is active
|
||||
*/
|
||||
public void setActive(boolean active)
|
||||
{
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the group id attribute name.
|
||||
*
|
||||
* @param groupIdAttributeName
|
||||
* the group id attribute name
|
||||
*/
|
||||
public void setGroupIdAttributeName(String groupIdAttributeName)
|
||||
{
|
||||
this.groupIdAttributeName = groupIdAttributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the group query.
|
||||
*
|
||||
* @param groupQuery
|
||||
* the group query
|
||||
*/
|
||||
public void setGroupQuery(String groupQuery)
|
||||
{
|
||||
this.groupQuery = groupQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the group differential query.
|
||||
*
|
||||
* @param groupDifferentialQuery
|
||||
* the group differential query
|
||||
*/
|
||||
public void setGroupDifferentialQuery(String groupDifferentialQuery)
|
||||
{
|
||||
this.groupDifferentialQuery = groupDifferentialQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the person query.
|
||||
*
|
||||
* @param personQuery
|
||||
* the person query
|
||||
*/
|
||||
public void setPersonQuery(String personQuery)
|
||||
{
|
||||
this.personQuery = personQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the person differential query.
|
||||
*
|
||||
* @param personDifferentialQuery
|
||||
* the person differential query
|
||||
*/
|
||||
public void setPersonDifferentialQuery(String personDifferentialQuery)
|
||||
{
|
||||
this.personDifferentialQuery = personDifferentialQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the group type.
|
||||
*
|
||||
* @param groupType
|
||||
* the group type
|
||||
*/
|
||||
public void setGroupType(String groupType)
|
||||
{
|
||||
this.groupType = groupType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the member attribute name.
|
||||
*
|
||||
* @param memberAttribute
|
||||
* the member attribute name
|
||||
*/
|
||||
public void setMemberAttribute(String memberAttribute)
|
||||
{
|
||||
this.memberAttributeName = memberAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the person type.
|
||||
*
|
||||
* @param personType
|
||||
* the person type
|
||||
*/
|
||||
public void setPersonType(String personType)
|
||||
{
|
||||
this.personType = personType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the group search base.
|
||||
*
|
||||
* @param groupSearchBase
|
||||
* the group search base
|
||||
*/
|
||||
public void setGroupSearchBase(String groupSearchBase)
|
||||
{
|
||||
this.groupSearchBase = groupSearchBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user search base.
|
||||
*
|
||||
* @param userSearchBase
|
||||
* the user search base
|
||||
*/
|
||||
public void setUserSearchBase(String userSearchBase)
|
||||
{
|
||||
this.userSearchBase = userSearchBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user id attribute name.
|
||||
*
|
||||
* @param userIdAttributeName
|
||||
* the user id attribute name
|
||||
*/
|
||||
public void setUserIdAttributeName(String userIdAttributeName)
|
||||
{
|
||||
this.userIdAttributeName = userIdAttributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the modification timestamp attribute name.
|
||||
*
|
||||
* @param modifyTimestampAttributeName
|
||||
* the modification timestamp attribute name
|
||||
*/
|
||||
public void setModifyTimestampAttributeName(String modifyTimestampAttributeName)
|
||||
{
|
||||
this.modifyTimestampAttributeName = modifyTimestampAttributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether to error on missing group members.
|
||||
*
|
||||
* @param errorOnMissingMembers
|
||||
* <code>true</code> if we should error on missing group members
|
||||
*/
|
||||
public void setErrorOnMissingMembers(boolean errorOnMissingMembers)
|
||||
{
|
||||
this.errorOnMissingMembers = errorOnMissingMembers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether to error on missing group IDs.
|
||||
*
|
||||
* @param errorOnMissingGID
|
||||
* <code>true</code> if we should error on missing group IDs
|
||||
*/
|
||||
public void setErrorOnMissingGID(boolean errorOnMissingGID)
|
||||
{
|
||||
this.errorOnMissingGID = errorOnMissingGID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether to error on missing user IDs.
|
||||
*
|
||||
* @param errorOnMissingUID
|
||||
* <code>true</code> if we should error on missing user IDs
|
||||
*/
|
||||
public void setErrorOnMissingUID(boolean errorOnMissingUID)
|
||||
{
|
||||
this.errorOnMissingUID = errorOnMissingUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether to error on duplicate group IDs.
|
||||
*
|
||||
* @param errorOnDuplicateGID
|
||||
* <code>true</code> if we should error on duplicate group IDs
|
||||
*/
|
||||
public void setErrorOnDuplicateGID(boolean errorOnDuplicateGID)
|
||||
{
|
||||
this.errorOnDuplicateGID = errorOnDuplicateGID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the LDAP initial dir context factory.
|
||||
*
|
||||
* @param ldapInitialDirContextFactory
|
||||
* the new LDAP initial dir context factory
|
||||
*/
|
||||
public void setLDAPInitialDirContextFactory(LDAPInitialDirContextFactory ldapInitialDirContextFactory)
|
||||
{
|
||||
this.ldapInitialContextFactory = ldapInitialDirContextFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attribute defaults.
|
||||
*
|
||||
* @param attributeDefaults
|
||||
* the attribute defaults
|
||||
*/
|
||||
public void setAttributeDefaults(Map<String, String> attributeDefaults)
|
||||
{
|
||||
this.attributeDefaults = attributeDefaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the namespace service.
|
||||
*
|
||||
* @param namespaceService
|
||||
* the namespace service
|
||||
*/
|
||||
public void setNamespaceService(NamespaceService namespaceService)
|
||||
{
|
||||
this.namespaceService = namespaceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attribute mapping.
|
||||
*
|
||||
* @param attributeMapping
|
||||
* the attribute mapping
|
||||
*/
|
||||
public void setAttributeMapping(Map<String, String> attributeMapping)
|
||||
{
|
||||
this.attributeMapping = attributeMapping;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive()
|
||||
*/
|
||||
public boolean isActive()
|
||||
{
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
public void afterPropertiesSet() throws Exception
|
||||
{
|
||||
Set<String> userAttributeSet = new TreeSet<String>();
|
||||
userAttributeSet.add(this.userIdAttributeName);
|
||||
userAttributeSet.add(this.modifyTimestampAttributeName);
|
||||
for (String attribute : this.attributeMapping.values())
|
||||
{
|
||||
if (attribute != null)
|
||||
{
|
||||
userAttributeSet.add(attribute);
|
||||
}
|
||||
}
|
||||
this.userAttributeNames = new String[userAttributeSet.size()];
|
||||
userAttributeSet.toArray(this.userAttributeNames);
|
||||
this.groupAttributeNames = new String[]
|
||||
{
|
||||
this.groupIdAttributeName, this.modifyTimestampAttributeName, this.memberAttributeName
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.repo.security.sync.UserRegistry#getPersons(java.util.Date)
|
||||
*/
|
||||
public Iterator<NodeDescription> getPersons(Date modifiedSince)
|
||||
{
|
||||
return new PersonIterator(modifiedSince);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.alfresco.repo.security.sync.UserRegistry#getGroups(java.util.Date)
|
||||
*/
|
||||
public Iterator<NodeDescription> getGroups(Date modifiedSince)
|
||||
{
|
||||
Map<String, NodeDescription> lookup = new TreeMap<String, NodeDescription>();
|
||||
InitialDirContext ctx = null;
|
||||
try
|
||||
{
|
||||
ctx = this.ldapInitialContextFactory.getDefaultIntialDirContext();
|
||||
|
||||
SearchControls userSearchCtls = new SearchControls();
|
||||
userSearchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
|
||||
userSearchCtls.setReturningAttributes(this.groupAttributeNames);
|
||||
|
||||
NamingEnumeration<SearchResult> searchResults;
|
||||
|
||||
if (modifiedSince == null)
|
||||
{
|
||||
searchResults = ctx.search(this.groupSearchBase, this.groupQuery, userSearchCtls);
|
||||
}
|
||||
else
|
||||
{
|
||||
searchResults = ctx.search(this.groupSearchBase, this.groupDifferentialQuery, new Object[]
|
||||
{
|
||||
LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.format(modifiedSince)
|
||||
}, userSearchCtls);
|
||||
}
|
||||
|
||||
LdapName groupDistinguishedNamePrefix = new LdapName(this.groupSearchBase);
|
||||
LdapName userDistinguishedNamePrefix = new LdapName(this.userSearchBase);
|
||||
|
||||
while (searchResults.hasMoreElements())
|
||||
{
|
||||
SearchResult result = searchResults.next();
|
||||
Attributes attributes = result.getAttributes();
|
||||
Attribute gidAttribute = attributes.get(this.groupIdAttributeName);
|
||||
if (gidAttribute == null)
|
||||
{
|
||||
if (this.errorOnMissingGID)
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"NodeDescription returned by group search does not have mandatory group id attribute "
|
||||
+ attributes);
|
||||
}
|
||||
else
|
||||
{
|
||||
LDAPUserRegistry.logger.warn("Missing GID on " + attributes);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
String gid = "GROUP_" + gidAttribute.get(0);
|
||||
|
||||
NodeDescription group = lookup.get(gid);
|
||||
if (group == null)
|
||||
{
|
||||
group = new NodeDescription();
|
||||
group.getProperties().put(ContentModel.PROP_AUTHORITY_NAME, gid);
|
||||
lookup.put(gid, group);
|
||||
}
|
||||
else if (this.errorOnDuplicateGID)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Duplicate group id found for " + gid);
|
||||
}
|
||||
else
|
||||
{
|
||||
LDAPUserRegistry.logger.warn("Duplicate gid found for " + gid + " -> merging definitions");
|
||||
}
|
||||
|
||||
Attribute modifyTimestamp = attributes.get(this.modifyTimestampAttributeName);
|
||||
if (modifyTimestamp != null)
|
||||
{
|
||||
group.setLastModified(LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.parse(modifyTimestamp.get()
|
||||
.toString()));
|
||||
}
|
||||
Set<String> childAssocs = group.getChildAssociations();
|
||||
|
||||
Attribute memAttribute = attributes.get(this.memberAttributeName);
|
||||
// check for null
|
||||
if (memAttribute != null)
|
||||
{
|
||||
for (int i = 0; i < memAttribute.size(); i++)
|
||||
{
|
||||
String attribute = (String) memAttribute.get(i);
|
||||
if (attribute != null)
|
||||
{
|
||||
LdapName distinguishedName = new LdapName(attribute);
|
||||
Attribute nameAttribute;
|
||||
|
||||
// If the user and group search bases are different we may be able to recognise user and
|
||||
// group DNs without a secondary lookup
|
||||
if (!this.userSearchBase.equals(this.groupSearchBase))
|
||||
{
|
||||
Attributes nameAttributes = distinguishedName.getRdn(distinguishedName.size() - 1)
|
||||
.toAttributes();
|
||||
|
||||
// Recognise user DNs
|
||||
if (distinguishedName.startsWith(userDistinguishedNamePrefix)
|
||||
&& (nameAttribute = nameAttributes.get(this.userIdAttributeName)) != null)
|
||||
{
|
||||
childAssocs.add((String) nameAttribute.get());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recognise group DNs
|
||||
if (distinguishedName.startsWith(groupDistinguishedNamePrefix)
|
||||
&& (nameAttribute = nameAttributes.get(this.groupIdAttributeName)) != null)
|
||||
{
|
||||
childAssocs.add("GROUP_" + nameAttribute.get());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't determine the name and type from the DN alone, try a directory lookup
|
||||
if (distinguishedName.startsWith(userDistinguishedNamePrefix)
|
||||
|| distinguishedName.startsWith(groupDistinguishedNamePrefix))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
Attributes childAttributes = ctx.getAttributes(attribute, new String[]
|
||||
{
|
||||
"objectclass", this.groupIdAttributeName, this.userIdAttributeName
|
||||
});
|
||||
String objectclass = (String) childAttributes.get("objectclass").get();
|
||||
if (objectclass.equals(this.personType))
|
||||
{
|
||||
nameAttribute = childAttributes.get(this.userIdAttributeName);
|
||||
if (nameAttribute == null)
|
||||
{
|
||||
if (this.errorOnMissingUID)
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"User missing user id attribute DN =" + attribute + " att = "
|
||||
+ this.userIdAttributeName);
|
||||
}
|
||||
else
|
||||
{
|
||||
LDAPUserRegistry.logger.warn("User missing user id attribute DN ="
|
||||
+ attribute + " att = " + this.userIdAttributeName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
childAssocs.add((String) nameAttribute.get());
|
||||
continue;
|
||||
}
|
||||
else if (objectclass.equals(this.groupType))
|
||||
{
|
||||
|
||||
nameAttribute = childAttributes.get(this.groupIdAttributeName);
|
||||
if (nameAttribute == null)
|
||||
{
|
||||
if (this.errorOnMissingGID)
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"Group returned by group search does not have mandatory group id attribute "
|
||||
+ attributes);
|
||||
}
|
||||
else
|
||||
{
|
||||
LDAPUserRegistry.logger.warn("Missing GID on " + childAttributes);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
childAssocs.add("GROUP_" + nameAttribute.get());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (NamingException e)
|
||||
{
|
||||
// Unresolvable name
|
||||
}
|
||||
}
|
||||
if (this.errorOnMissingMembers)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Failed to resolve distinguished name: " + attribute);
|
||||
}
|
||||
LDAPUserRegistry.logger.warn("Failed to resolve distinguished name: " + attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LDAPUserRegistry.logger.isDebugEnabled())
|
||||
{
|
||||
LDAPUserRegistry.logger.debug("Found " + lookup.size());
|
||||
}
|
||||
|
||||
return lookup.values().iterator();
|
||||
}
|
||||
catch (NamingException e)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("User and group import failed", e);
|
||||
}
|
||||
catch (ParseException e)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("User and group import failed", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ctx != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ctx.close();
|
||||
}
|
||||
catch (NamingException e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the LDAP user query as an {@link Iterator}.
|
||||
*/
|
||||
public class PersonIterator implements Iterator<NodeDescription>
|
||||
{
|
||||
|
||||
/** The directory context. */
|
||||
private InitialDirContext ctx;
|
||||
|
||||
/** The search results. */
|
||||
private NamingEnumeration<SearchResult> searchResults;
|
||||
|
||||
/** The uids. */
|
||||
private HashSet<String> uids = new HashSet<String>();
|
||||
|
||||
/** The next node description to return. */
|
||||
private NodeDescription next;
|
||||
|
||||
/**
|
||||
* Instantiates a new person iterator.
|
||||
*
|
||||
* @param modifiedSince
|
||||
* if non-null, then only descriptions of users modified since this date should be returned; if
|
||||
* <code>null</code> then descriptions of all users should be returned.
|
||||
*/
|
||||
public PersonIterator(Date modifiedSince)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.ctx = LDAPUserRegistry.this.ldapInitialContextFactory.getDefaultIntialDirContext();
|
||||
|
||||
// Authentication has been successful.
|
||||
// Set the current user, they are now authenticated.
|
||||
|
||||
SearchControls userSearchCtls = new SearchControls();
|
||||
userSearchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
|
||||
userSearchCtls.setCountLimit(Integer.MAX_VALUE);
|
||||
userSearchCtls.setReturningAttributes(LDAPUserRegistry.this.userAttributeNames);
|
||||
|
||||
if (modifiedSince == null)
|
||||
{
|
||||
this.searchResults = this.ctx.search(LDAPUserRegistry.this.userSearchBase,
|
||||
LDAPUserRegistry.this.personQuery, userSearchCtls);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.searchResults = this.ctx.search(LDAPUserRegistry.this.userSearchBase,
|
||||
LDAPUserRegistry.this.personDifferentialQuery, new Object[]
|
||||
{
|
||||
LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.format(modifiedSince)
|
||||
}, userSearchCtls);
|
||||
}
|
||||
this.next = fetchNext();
|
||||
}
|
||||
catch (NamingException e)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Failed to import people.", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (this.searchResults == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.ctx.close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
this.ctx = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.Iterator#hasNext()
|
||||
*/
|
||||
public boolean hasNext()
|
||||
{
|
||||
return this.next != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.Iterator#next()
|
||||
*/
|
||||
public NodeDescription next()
|
||||
{
|
||||
if (this.next == null)
|
||||
{
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
NodeDescription current = this.next;
|
||||
try
|
||||
{
|
||||
this.next = fetchNext();
|
||||
}
|
||||
catch (NamingException e)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Failed to import people.", e);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fetches the next node description to be returned.
|
||||
*
|
||||
* @return the node description
|
||||
* @throws NamingException
|
||||
* on a naming exception
|
||||
*/
|
||||
private NodeDescription fetchNext() throws NamingException
|
||||
{
|
||||
while (this.searchResults.hasMoreElements())
|
||||
{
|
||||
SearchResult result = this.searchResults.next();
|
||||
Attributes attributes = result.getAttributes();
|
||||
Attribute uidAttribute = attributes.get(LDAPUserRegistry.this.userIdAttributeName);
|
||||
if (uidAttribute == null)
|
||||
{
|
||||
if (LDAPUserRegistry.this.errorOnMissingUID)
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"User returned by user search does not have mandatory user id attribute " + attributes);
|
||||
}
|
||||
else
|
||||
{
|
||||
LDAPUserRegistry.logger
|
||||
.warn("User returned by user search does not have mandatory user id attribute "
|
||||
+ attributes);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
String uid = (String) uidAttribute.get(0);
|
||||
|
||||
if (this.uids.contains(uid))
|
||||
{
|
||||
LDAPUserRegistry.logger
|
||||
.warn("Duplicate uid found - there will be more than one person object for this user - "
|
||||
+ uid);
|
||||
}
|
||||
|
||||
this.uids.add(uid);
|
||||
|
||||
if (LDAPUserRegistry.logger.isDebugEnabled())
|
||||
{
|
||||
LDAPUserRegistry.logger.debug("Adding user for " + uid);
|
||||
}
|
||||
|
||||
NodeDescription person = new NodeDescription();
|
||||
|
||||
Attribute modifyTimestamp = attributes.get(LDAPUserRegistry.this.modifyTimestampAttributeName);
|
||||
if (modifyTimestamp != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
person.setLastModified(LDAPUserRegistry.LDAP_GENERALIZED_TIME_FORMAT.parse(modifyTimestamp
|
||||
.get().toString()));
|
||||
}
|
||||
catch (ParseException e)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Failed to import people.", e);
|
||||
}
|
||||
}
|
||||
|
||||
PropertyMap properties = person.getProperties();
|
||||
for (String key : LDAPUserRegistry.this.attributeMapping.keySet())
|
||||
{
|
||||
QName keyQName = QName.createQName(key, LDAPUserRegistry.this.namespaceService);
|
||||
|
||||
// cater for null
|
||||
String attributeName = LDAPUserRegistry.this.attributeMapping.get(key);
|
||||
if (attributeName != null)
|
||||
{
|
||||
Attribute attribute = attributes.get(attributeName);
|
||||
if (attribute != null)
|
||||
{
|
||||
String value = (String) attribute.get(0);
|
||||
if (value != null)
|
||||
{
|
||||
properties.put(keyQName, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String defaultValue = LDAPUserRegistry.this.attributeDefaults.get(key);
|
||||
if (defaultValue != null)
|
||||
{
|
||||
properties.put(keyQName, defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String defaultValue = LDAPUserRegistry.this.attributeDefaults.get(key);
|
||||
if (defaultValue != null)
|
||||
{
|
||||
properties.put(keyQName, defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
return person;
|
||||
}
|
||||
this.searchResults.close();
|
||||
this.searchResults = null;
|
||||
this.ctx.close();
|
||||
this.ctx = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.Iterator#remove()
|
||||
*/
|
||||
public void remove()
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user