alfresco-community-repo/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java
Dave Ward 85c1b71826 Merged V3.2 to HEAD
16662: LDAP sync: improved group association filtering, referential integrity checking, deletion strategy and performance tuning of batch sizes
   16648: ETHREEOH-2752: Improved ticket validation fix
      - Invalidate user's tickets during person deletion rather than validation or it can mess up chained validation
   16647: ETHREEOH-2534: Fixed Sharepoint NTLM authentication
      - user details were never getting cached in the session
   16579: Small improvement to LDAP error reporting
      - Committed errors counted before successes in a logging interval
   16515: LDAP sync performance
      - Improved full sync strategy - run differential queries to work out required updates/additions and full queries to work out required deletions. Saves updating unchanged nodes.
      - Use a TreeSet rather than a HashSet to gather group associations in an attempt to avoid blowing the heap size
   16498: More LDAP performance improvements
      - Uses thread pool with 4 worker threads and blocking queue to process returned results. The number of worker threads can be controlled by the synchronization.workerThreads property.
      - Switched LDAP connection pooling back on again
      - Group Associations processsed individually so that errors are collated and we get a better idea of their throughput
      - Fixed potential bug. Group membership resolution done with isolated LDAP context to avoid cookies from paging creeping in.
   16424: Try switching off LDAP connection pooling to see if it works better with our flaky server.
   16414: Further LDAP fault tolerance
      - Log causes of group member resolution failures where possible
   16413: More fault tolerance for LDAP sync
      - Always commit last sync times before overall sync is complete to avoid the 'forgetting' of differential sync information
      - DN comparisons should be case insensitive to avoid issues resolving DNs to user and group IDs
   16398: Improved monitoring and fault tolerance for LDAP sync
      - When the batch is complete a summary of the number of errors and the last error stack trace will be logged at ERROR level
      - Each individual error is logged at WARN level and progress information (including % complete) is collated and logged at INFO level after a configurable interval
      - In the Enterprise Edition all metrics can be monitored in real time through JMX
      - Sanity testing to be performed by Mike!
   16319: Merged HEAD to V3.2
      16316: ALFCOM-3397: JBoss 5 compatibility fix
         - Relative paths used by LDAP subsystem configuration weren't being resolved correctly
         - See also https://jira.jboss.org/jira/browse/JBAS-6548 and https://jira.springsource.org/browse/SPR-5120
   16272: ETHREEOH-2752: Once more with feeling!
   16261: ETHREEOH-2752: Correct exception propagation.
   16260: ETHREEOH-2752: Fix ticket validation
      - Current ticket was getting forgotten by previous fix
      - Person validation in CHECK mode now done AFTER the current user is set, so that the current ticket is remembered
   16243: ETHREEOH-2752: Improve ticket validation used by all authentication filters
      - Now takes into account whether person actually exists or not
      - Tickets for non-nonexistent persons are now considered invalid and cached session information is invalidated
      - New BaseAuthenticationFilter superclass for all authentication filters
      - Improved fix to ETHREEOH-2839: WebDAV user is cached consistently using a different session attribute from the Web Client
   16233: ETHREEOH-2754: Correction to previous checkin.
      - relogin for SSO authentication, logout for normal login page
      - logout is default
   16232: ETHREEOH-2754: Log Out Action outcome passed as a parameter
      - relogin for SSO authentication, login for normal login page
      - Means the log out link always leads to the correct place, even when the session has expired
      - Also lowered ticket validation error logging to DEBUG level to avoid unnecessary noise in the logs from expired sessions
   16220: ETHREEOH-2839: Fixed potential ClassCastExceptions when Alfresco accessed via WebDAV and Web Client links in same browser
      - WebDAV side no longer directly casts session user to a WebDAVUser
      - ContextListener no longer casts session user to web client user
      - Web client side will 'promote' session user to a web client User if necessary via AuthenticationHelper
      - All authentication filters made to use appropriate AuthenticationHelper methods
   16211: ETHREEOH-2835: LDAP sync batches user and group deletions as well as creations
      - Also improved logging of sync failures
   16197: ETHREEOH-2782: LDAP subsystems now support search-based user DN resolution
      - When ldap.authentication.userNameFormat isn't set (now the default) converts a user ID to a DN by running ldap.synchronization.personQuery with an extra condition tacked on the end to find the user by ID
      - Structured directories and authentication by attributes not in the DN such as email address now supported
   16189: ALFCOM-3283: Prevent errors when user accepts an invite when not logged in
      - new isGuest attribute propagated to user object
      - header component (used by accept-invite page) needs to avoid calling prefs and site webscripts for guest user
      - Conditional stuff in header template changed to use user.isGuest


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@16896 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2009-10-14 09:24:13 +00:00

1032 lines
49 KiB
Java

/*
* 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.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
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.lock.JobLockService;
import org.alfresco.repo.lock.LockAcquisitionException;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.management.subsystems.ChildApplicationContextManager;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.sync.BatchProcessor.Worker;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
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.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.AbstractLifecycleBean;
import org.alfresco.util.Pair;
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;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
/**
* 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. The
* {@link JobLockService} is used to ensure that in a cluster, no two nodes actually run a synchronize at the same time.
* <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 on subsystem startup and also by
* {@link #createMissingPerson(String)} 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 extends AbstractLifecycleBean implements UserRegistrySynchronizer,
ApplicationEventPublisherAware
{
/** The logger. */
private static final Log logger = LogFactory.getLog(ChainingUserRegistrySynchronizer.class);
/** The name of the lock used to ensure that a synchronize does not run on more than one node at the same time. */
private static final QName LOCK_QNAME = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI,
"ChainingUserRegistrySynchronizer");
/** The maximum time this lock will be held for (1 day). */
private static final long LOCK_TTL = 1000 * 60 * 60 * 24;
/** 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;
/** The retrying transaction helper. */
private RetryingTransactionHelper retryingTransactionHelper;
/** The job lock service. */
private JobLockService jobLockService;
/** The application event publisher. */
private ApplicationEventPublisher applicationEventPublisher;
/** Should we trigger a differential sync when missing people log in?. */
private boolean syncWhenMissingPeopleLogIn = true;
/** Should we trigger a differential sync on startup?. */
private boolean syncOnStartup = true;
/** Should we auto create a missing person on log in?. */
private boolean autoCreatePeopleOnLogin = true;
/** The number of entries to process before reporting progress. */
private int loggingInterval = 100;
/** The number of worker threads. */
private int workerThreads = 2;
/**
* 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;
}
/**
* Sets the retrying transaction helper.
*
* @param retryingTransactionHelper
* the new retrying transaction helper
*/
public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper)
{
this.retryingTransactionHelper = retryingTransactionHelper;
}
/**
* Sets the job lock service.
*
* @param jobLockService
* the job lock service
*/
public void setJobLockService(JobLockService jobLockService)
{
this.jobLockService = jobLockService;
}
/*
* (non-Javadoc)
* @see
* org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context
* .ApplicationEventPublisher)
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher)
{
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Controls whether we auto create a missing person on log in.
*
* @param autoCreatePeopleOnLogin
* <code>true</code> if we should auto create a missing person on log in
*/
public void setAutoCreatePeopleOnLogin(boolean autoCreatePeopleOnLogin)
{
this.autoCreatePeopleOnLogin = autoCreatePeopleOnLogin;
}
/**
* Controls whether we trigger a differential sync when missing people log in.
*
* @param syncWhenMissingPeopleLogIn
* <codetrue</code> if we should trigger a sync when missing people log in
*/
public void setSyncWhenMissingPeopleLogIn(boolean syncWhenMissingPeopleLogIn)
{
this.syncWhenMissingPeopleLogIn = syncWhenMissingPeopleLogIn;
}
/**
* Controls whether we trigger a differential sync when the subsystem starts up.
*
* @param syncOnStartup
* <codetrue</code> if we should trigger a sync on startup
*/
public void setSyncOnStartup(boolean syncOnStartup)
{
this.syncOnStartup = syncOnStartup;
}
/**
* Sets the number of entries to process before reporting progress.
*
* @param loggingInterval
* the number of entries to process before reporting progress or zero to disable progress reporting.
*/
public void setLoggingInterval(int loggingInterval)
{
this.loggingInterval = loggingInterval;
}
/**
* Sets the number of worker threads.
*
* @param workerThreads
* the number of worker threads
*/
public void setWorkerThreads(int workerThreads)
{
this.workerThreads = workerThreads;
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#synchronize(boolean, boolean)
*/
public void synchronize(boolean force, boolean splitTxns)
{
// Let's ensure all exceptions get logged
try
{
// First, try to obtain a lock to ensure we are the only node trying to run this job
try
{
if (splitTxns)
{
// If this is an automated sync on startup or scheduled sync, don't even wait around for the lock.
// Assume the sync will be completed on another node.
this.jobLockService.getTransactionalLock(ChainingUserRegistrySynchronizer.LOCK_QNAME,
ChainingUserRegistrySynchronizer.LOCK_TTL, 0, 1);
}
else
{
// If this is a login-triggered sync, give it a few retries before giving up
this.jobLockService.getTransactionalLock(ChainingUserRegistrySynchronizer.LOCK_QNAME,
ChainingUserRegistrySynchronizer.LOCK_TTL, 3000, 10);
}
}
catch (LockAcquisitionException e)
{
// Don't proceed with the sync if it is running on another node
ChainingUserRegistrySynchronizer.logger
.warn("User registry synchronization already running in another thread. Synchronize aborted");
return;
}
Set<String> visitedZoneIds = new TreeSet<String>();
Collection<String> instanceIds = this.applicationContextManager.getInstanceIds();
// Work out the set of all zone IDs in the authentication chain so that we can decide which users / groups
// need 're-zoning'
Set<String> allZoneIds = new TreeSet<String>();
for (String id : instanceIds)
{
allZoneIds.add(AuthorityService.ZONE_AUTH_EXT_PREFIX + id);
}
for (String id : instanceIds)
{
ApplicationContext context = this.applicationContextManager.getApplicationContext(id);
try
{
UserRegistry plugin = (UserRegistry) context.getBean(this.sourceBeanName);
if (!(plugin instanceof ActivateableBean) || ((ActivateableBean) plugin).isActive())
{
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
ChainingUserRegistrySynchronizer.logger
.info("Synchronizing users and groups with user registry '" + id + "'");
}
if (force && ChainingUserRegistrySynchronizer.logger.isWarnEnabled())
{
ChainingUserRegistrySynchronizer.logger
.warn("Forced synchronization with user registry '"
+ id
+ "'; some users and groups previously created by synchronization with this user registry may be removed.");
}
// Work out whether we should do the work in a separate transaction (it's most performant if we
// bunch it into small transactions, but if we are doing a sync on login, it has to be the same
// transaction)
boolean requiresNew = splitTxns
|| AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
int personsProcessed = syncPersonsWithPlugin(id, plugin, requiresNew, visitedZoneIds,
allZoneIds);
int groupsProcessed = syncGroupsWithPlugin(id, plugin, force, requiresNew, visitedZoneIds,
allZoneIds);
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
ChainingUserRegistrySynchronizer.logger
.info("Finished synchronizing users and groups with user registry '" + id + "'");
ChainingUserRegistrySynchronizer.logger.info(personsProcessed + " user(s) and "
+ groupsProcessed + " group(s) processed");
}
}
}
catch (NoSuchBeanDefinitionException e)
{
// Ignore and continue
}
}
}
catch (RuntimeException e)
{
ChainingUserRegistrySynchronizer.logger.error("Synchronization aborted due to error", e);
throw e;
}
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#ensureExists(java.lang.String)
*/
public boolean createMissingPerson(String userName)
{
// synchronize or auto-create the missing person if we are allowed
if (userName != null && !userName.equals(AuthenticationUtil.getSystemUserName()))
{
if (this.syncWhenMissingPeopleLogIn)
{
try
{
synchronize(false, false);
}
catch (Exception e)
{
// We don't want to fail the whole login if we can help it
ChainingUserRegistrySynchronizer.logger.warn(
"User authenticated but failed to sync with user registry", e);
}
if (this.personService.personExists(userName))
{
return true;
}
}
if (this.autoCreatePeopleOnLogin && this.personService.createMissingPeople())
{
AuthorityType authorityType = AuthorityType.getAuthorityType(userName);
if (authorityType == AuthorityType.USER)
{
this.personService.getPerson(userName);
return true;
}
}
}
return false;
}
/**
* Synchronizes changes only to local users (persons) with a {@link UserRegistry} for a particular zone.
*
* @param zone
* 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 splitTxns
* Can the modifications to Alfresco be split across multiple transactions for maximum performance? If
* <code>true</code>, users and groups are created/updated in batches of 10 for increased performance. If
* <code>false</code>, all users and groups are processed in the current transaction. This is required if
* calling synchronously (e.g. in response to an authentication event in the same transaction).
* @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.
* @param allZoneIds
* the set of all zone ids in the authentication chain. Helps us work out whether the zone information
* recorded against a user is invalid for the current authentication chain and whether the user needs to
* be 're-zoned'.
* @return the number of users processed
*/
private int syncPersonsWithPlugin(final String zone, UserRegistry userRegistry, boolean splitTxns,
final Set<String> visitedZoneIds, final Set<String> allZoneIds)
{
// Create a prefixed zone ID for use with the authority service
final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;
// The set of zones we associate with new objects (default plus registry specific)
final Set<String> zoneSet = getZones(zoneId);
final long lastModifiedMillis = getMostRecentUpdateTime(
ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId);
final Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
if (lastModified == null)
{
ChainingUserRegistrySynchronizer.logger.info("Retrieving all users from user registry '" + zone + "'");
}
else
{
ChainingUserRegistrySynchronizer.logger.info("Retrieving users changed since "
+ DateFormat.getDateTimeInstance().format(lastModified) + " from user registry '" + zone + "'");
}
}
final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(
this.retryingTransactionHelper, this.applicationEventPublisher, userRegistry.getPersons(lastModified),
zone + " User Creation", this.loggingInterval, this.workerThreads, 10);
class CreationWorker implements Worker<NodeDescription>
{
private long latestTime = lastModifiedMillis;
public long getLatestTime()
{
return this.latestTime;
}
public String getIdentifier(NodeDescription entry)
{
return entry.getSourceId();
}
public void process(NodeDescription person) throws Throwable
{
PropertyMap personProperties = person.getProperties();
String personName = (String) personProperties.get(ContentModel.PROP_USERNAME);
Set<String> zones = ChainingUserRegistrySynchronizer.this.authorityService
.getAuthorityZones(personName);
if (zones == null)
{
// The person did not exist at all
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled())
{
ChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'");
}
ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet);
}
else if (zones.contains(zoneId))
{
// The person already existed in this zone: update the person
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled())
{
ChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'");
}
ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
personProperties);
}
else
{
// Check whether the user is in any of the authentication chain zones
Set<String> intersection = new TreeSet<String>(zones);
intersection.retainAll(allZoneIds);
if (intersection.size() == 0)
{
// The person exists, but not in a zone that's in the authentication chain. May be due
// to upgrade or zone changes. Let's re-zone them
if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled())
{
ChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName
+ "'. This user will in future be assumed to originate from user registry '" + zone
+ "'.");
}
ChainingUserRegistrySynchronizer.this.authorityService.removeAuthorityFromZones(personName,
zones);
ChainingUserRegistrySynchronizer.this.authorityService.addAuthorityToZones(personName, zoneSet);
ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
personProperties);
}
else
{
// Check whether the user is in any of the higher priority authentication chain zones
intersection.retainAll(visitedZoneIds);
if (intersection.size() > 0)
{
// A person that exists in a different zone with higher precedence - ignore
return;
}
// The person existed, but in a zone with lower precedence
if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled())
{
ChainingUserRegistrySynchronizer.logger
.warn("Recreating occluded user '"
+ personName
+ "'. This user was previously created through synchronization with a lower priority user registry.");
}
ChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet);
}
}
synchronized (this)
{
// Maintain the last modified date
Date personLastModified = person.getLastModified();
if (personLastModified != null)
{
this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
}
}
}
}
CreationWorker creations = new CreationWorker();
int processedCount = personProcessor.process(creations, splitTxns);
long latestTime = creations.getLatestTime();
if (latestTime != -1)
{
setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
latestTime, splitTxns);
}
// Remember we have visited this zone
visitedZoneIds.add(zoneId);
return processedCount;
}
/**
* Synchronizes local groups with a {@link UserRegistry} for a particular zone and also handles deletions of local
* groups and users.
*
* @param zone
* 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 user and group deletions are to be processed.
* @param splitTxns
* Can the modifications to Alfresco be split across multiple transactions for maximum performance? If
* <code>true</code>, users and groups are created/updated in batches of 10 for increased performance. If
* <code>false</code>, all users and groups are processed in the current transaction. This is required if
* calling synchronously (e.g. in response to an authentication event in the same transaction).
* @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.
* @param allZoneIds
* the set of all zone ids in the authentication chain. Helps us work out whether the zone information
* recorded against a group is invalid for the current authentication chain and whether the group needs
* to be 're-zoned'.
* @return the number of groups processed
*/
private int syncGroupsWithPlugin(final String zone, UserRegistry userRegistry, boolean force, boolean splitTxns,
final Set<String> visitedZoneIds, final Set<String> allZoneIds)
{
// Create a prefixed zone ID for use with the authority service
final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;
// The set of zones we associate with new objects (default plus registry specific)
final Set<String> zoneSet = getZones(zoneId);
final long lastModifiedMillis = getMostRecentUpdateTime(
ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId);
final Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
if (lastModified == null)
{
ChainingUserRegistrySynchronizer.logger.info("Retrieving all groups from user registry '" + zone + "'");
}
else
{
ChainingUserRegistrySynchronizer.logger.info("Retrieving groups changed since "
+ DateFormat.getDateTimeInstance().format(lastModified) + " from user registry '" + zone + "'");
}
}
// Get current set of known authorities
Set<String> deletionCandidates = this.retryingTransactionHelper.doInTransaction(
new RetryingTransactionCallback<Set<String>>()
{
public Set<String> execute() throws Throwable
{
return ChainingUserRegistrySynchronizer.this.authorityService.getAllAuthoritiesInZone(zoneId,
null);
}
}, false, splitTxns);
final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(
this.retryingTransactionHelper, this.applicationEventPublisher, userRegistry.getGroups(lastModified,
deletionCandidates, force), zone + " Group Creation", this.loggingInterval, this.workerThreads,
20);
class CreationWorker implements Worker<NodeDescription>
{
private final Set<Pair<String, String>> groupAssocsToCreate = new TreeSet<Pair<String, String>>(
new Comparator<Pair<String, String>>()
{
public int compare(Pair<String, String> o1, Pair<String, String> o2)
{
int result = o1.getFirst().compareTo(o2.getFirst());
if (result == 0)
{
return o1.getSecond().compareTo(o2.getSecond());
}
return result;
}
});
private long latestTime = lastModifiedMillis;
public long getLatestTime()
{
return this.latestTime;
}
public Set<Pair<String, String>> getGroupAssocsToCreate()
{
return this.groupAssocsToCreate;
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.security.sync.BatchProcessor.Worker#getIdentifier(java.lang.Object)
*/
public String getIdentifier(NodeDescription entry)
{
return entry.getSourceId();
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.security.sync.BatchProcessor.Worker#process(java.lang.Object)
*/
public void process(NodeDescription group) throws Throwable
{
PropertyMap groupProperties = group.getProperties();
String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService.getShortName(groupName);
Set<String> groupZones = ChainingUserRegistrySynchronizer.this.authorityService
.getAuthorityZones(groupName);
if (groupZones == null)
{
// The group did not exist at all
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled())
{
ChainingUserRegistrySynchronizer.logger.debug("Creating group '" + groupShortName + "'");
}
// create the group
ChainingUserRegistrySynchronizer.this.authorityService.createAuthority(AuthorityType
.getAuthorityType(groupName), groupShortName, (String) groupProperties
.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME), zoneSet);
Set<String> children = group.getChildAssociations();
if (!children.isEmpty())
{
synchronized (this)
{
for (String child : children)
{
this.groupAssocsToCreate.add(new Pair<String, String>(groupName, child));
}
}
}
}
else
{
// Check whether the group is in any of the authentication chain zones
Set<String> intersection = new TreeSet<String>(groupZones);
intersection.retainAll(allZoneIds);
if (intersection.isEmpty())
{
// The group exists, but not in a zone that's in the authentication chain. May be due to
// upgrade or zone changes. Let's re-zone them
if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled())
{
ChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName
+ "'. This group will in future be assumed to originate from user registry '"
+ zone + "'.");
}
ChainingUserRegistrySynchronizer.this.authorityService.removeAuthorityFromZones(groupName,
groupZones);
ChainingUserRegistrySynchronizer.this.authorityService.addAuthorityToZones(groupName, zoneSet);
}
if (groupZones.contains(zoneId) || intersection.isEmpty())
{
// The group already existed in this zone or no valid zone: update the group
Set<String> oldChildren = ChainingUserRegistrySynchronizer.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())
{
synchronized (this)
{
for (String child : toAdd)
{
this.groupAssocsToCreate.add(new Pair<String, String>(groupName, child));
}
}
}
for (String child : toDelete)
{
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled())
{
ChainingUserRegistrySynchronizer.logger.debug("Removing '"
+ ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child)
+ "' from group '" + groupShortName + "'");
}
ChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(groupName, child);
}
}
else
{
// Check whether the group is in any of the higher priority authentication chain zones
intersection.retainAll(visitedZoneIds);
if (!intersection.isEmpty())
{
// A group that exists in a different zone with higher precedence
return;
}
// The group existed, but in a zone with lower precedence
if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled())
{
ChainingUserRegistrySynchronizer.logger
.warn("Recreating occluded group '"
+ groupShortName
+ "'. This group was previously created through synchronization with a lower priority user registry.");
}
ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName);
// create the group
ChainingUserRegistrySynchronizer.this.authorityService.createAuthority(AuthorityType
.getAuthorityType(groupName), groupShortName, (String) groupProperties
.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME), zoneSet);
Set<String> children = group.getChildAssociations();
if (!children.isEmpty())
{
synchronized (this)
{
for (String child : children)
{
this.groupAssocsToCreate.add(new Pair<String, String>(groupName, child));
}
}
}
}
}
synchronized (this)
{
// Maintain the last modified date
Date groupLastModified = group.getLastModified();
if (groupLastModified != null)
{
this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
}
}
}
}
CreationWorker creations = new CreationWorker();
int processedCount = groupProcessor.process(creations, splitTxns);
long latestTime = creations.getLatestTime();
if (latestTime != -1)
{
setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, latestTime,
splitTxns);
}
// Add the new associations, now that we have created everything
BatchProcessor<Pair<String, String>> groupAssocProcessor = new BatchProcessor<Pair<String, String>>(
this.retryingTransactionHelper, this.applicationEventPublisher, creations.getGroupAssocsToCreate(),
zone + " Group Association Creation", this.loggingInterval, this.workerThreads, 20);
groupAssocProcessor.process(new Worker<Pair<String, String>>()
{
public String getIdentifier(Pair<String, String> entry)
{
return entry.toString();
}
public void process(Pair<String, String> entry) throws Throwable
{
String groupName = entry.getFirst();
String child = entry.getSecond();
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled())
{
ChainingUserRegistrySynchronizer.logger.debug("Adding '"
+ ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child)
+ "' to group '"
+ ChainingUserRegistrySynchronizer.this.authorityService.getShortName(groupName) + "'");
}
ChainingUserRegistrySynchronizer.this.authorityService.addAuthority(groupName, child);
}
}, splitTxns);
// Delete authorities if we have complete information for the zone
if (force)
{
BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(
this.retryingTransactionHelper, this.applicationEventPublisher, deletionCandidates, zone
+ " Authority Deletion", this.loggingInterval, this.workerThreads, 10);
processedCount += authorityDeletionProcessor.process(new Worker<String>()
{
public String getIdentifier(String entry)
{
return entry;
}
public void process(String authority) throws Throwable
{
if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER)
{
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled())
{
ChainingUserRegistrySynchronizer.logger.debug("Deleting user '" + authority + "'");
}
ChainingUserRegistrySynchronizer.this.personService.deletePerson(authority);
}
else
{
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled())
{
ChainingUserRegistrySynchronizer.logger.debug("Deleting group '"
+ ChainingUserRegistrySynchronizer.this.authorityService.getShortName(authority)
+ "'");
}
ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
}
}
}, splitTxns);
}
// Remember we have visited this zone
visitedZoneIds.add(zoneId);
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
* @param splitTxns
* Can the modifications to Alfresco be split across multiple transactions for maximum performance? If
* <code>true</code>, the attribute is persisted in a new transaction for increased performance and
* reliability.
*/
private void setMostRecentUpdateTime(final String label, final String zoneId, final long lastModifiedMillis,
boolean splitTxns)
{
final String path = ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH + '/' + label;
this.retryingTransactionHelper.doInTransaction(
new RetryingTransactionHelper.RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
if (!ChainingUserRegistrySynchronizer.this.attributeService.exists(path))
{
if (!ChainingUserRegistrySynchronizer.this.attributeService
.exists(ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH))
{
ChainingUserRegistrySynchronizer.this.attributeService.setAttribute("",
ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, new MapAttributeValue());
}
ChainingUserRegistrySynchronizer.this.attributeService.setAttribute(
ChainingUserRegistrySynchronizer.ROOT_ATTRIBUTE_PATH, label,
new MapAttributeValue());
}
ChainingUserRegistrySynchronizer.this.attributeService.setAttribute(path, zoneId,
new LongAttributeValue(lastModifiedMillis));
return null;
}
}, false, splitTxns);
}
/**
* Gets the default set of zones to set on a person or group belonging to the user registry with the given zone ID.
* We add the default zone as well as the zone corresponding to the user registry so that the users and groups are
* visible in the UI.
*
* @param zoneId
* the zone id
* @return the zone set
*/
private Set<String> getZones(String zoneId)
{
Set<String> zones = new HashSet<String>(5);
zones.add(AuthorityService.ZONE_APP_DEFAULT);
zones.add(zoneId);
return zones;
}
/*
* (non-Javadoc)
* @see org.alfresco.util.AbstractLifecycleBean#onBootstrap(org.springframework.context.ApplicationEvent)
*/
@Override
protected void onBootstrap(ApplicationEvent event)
{
// Do an initial differential sync on startup, using transaction splitting. This ensures that on the very
// first startup, we don't have to wait for a very long login operation to trigger the first sync!
if (this.syncOnStartup)
{
AuthenticationUtil.runAs(new RunAsWork<Object>()
{
public Object doWork() throws Exception
{
return ChainingUserRegistrySynchronizer.this.retryingTransactionHelper
.doInTransaction(new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
try
{
synchronize(false, true);
}
catch (Exception e)
{
ChainingUserRegistrySynchronizer.logger.warn(
"Failed initial synchronize with user registries", e);
}
return null;
}
});
}
}, AuthenticationUtil.getSystemUserName());
}
}
/*
* (non-Javadoc)
* @see org.alfresco.util.AbstractLifecycleBean#onShutdown(org.springframework.context.ApplicationEvent)
*/
@Override
protected void onShutdown(ApplicationEvent event)
{
}
}