use of org.alfresco.repo.security.sync.ldap.LDAPUserRegistry in project alfresco-repository by Alfresco.
the class LDAPUserRegistryTest method createRegistry.
private LDAPUserRegistry createRegistry() throws Exception {
LDAPUserRegistry registry = new LDAPUserRegistry();
registry.setLDAPInitialDirContextFactory(contextFactory);
registry.setNamespaceService(namespaceService);
registry.setGroupSearchBase(GROUP_SEARCH_BASE);
registry.setUserSearchBase(USER_SEARCH_BASE);
registry.setGroupQuery(GROUP_QUERY);
registry.setPersonQuery(PERSON_QUERY);
registry.setGroupDifferentialQuery(GROUP_DIFFERENTIAL_QUERY);
registry.setPersonDifferentialQuery(PERSON_DIFFERENTIAL_QUERY);
registry.setGroupIdAttributeName(GROUP_ID_ATTRIBUTE_NAME);
registry.setUserIdAttributeName(USER_ID_ATTRIBUTE_NAME);
registry.setMemberAttribute(MEMBER_ATTRIBUTE_NAME);
Set<String> prefixes = new HashSet<>();
prefixes.add(NamespaceService.CONTENT_MODEL_PREFIX);
when(namespaceService.getPrefixes(NamespaceService.CONTENT_MODEL_1_0_URI)).thenReturn(prefixes);
when(namespaceService.getNamespaceURI(NamespaceService.CONTENT_MODEL_PREFIX)).thenReturn(NamespaceService.CONTENT_MODEL_1_0_URI);
when(contextFactory.getDefaultIntialDirContext()).thenReturn(initialDirContext);
when(contextFactory.getDefaultIntialDirContext(0)).thenReturn(initialDirContext);
when(initialDirContext.search(eq(GROUP_SEARCH_BASE), eq(GROUP_DIFFERENTIAL_QUERY), any())).thenReturn(searchResults);
when(searchResults.hasMore()).thenReturn(true);
when(searchResults.next()).thenReturn(searchResult);
when(searchResult.getAttributes()).thenReturn(attributes);
when(attributes.get(GROUP_ID_ATTRIBUTE_NAME)).thenReturn(groupAttribute);
when(groupAttribute.get(0)).thenReturn(GROUP_ATTRIBUTE);
when(attributes.get(MEMBER_ATTRIBUTE_NAME)).thenReturn(rangeRestrictedAttribute);
when(rangeRestrictedAttribute.size()).thenReturn(1);
when(rangeRestrictedAttribute.get(0)).thenReturn(MEMBER_ATTRIBUTE_VALUE);
registry.afterPropertiesSet();
return registry;
}
use of org.alfresco.repo.security.sync.ldap.LDAPUserRegistry in project alfresco-repository by Alfresco.
the class ChainingUserRegistrySynchronizer method syncWithPlugin.
/**
* Synchronizes local groups and users with a {@link UserRegistry} for a particular zone, optionally handling
* deletions.
*
* @param zone
* the zone id. This identifier is used to tag all created groups and 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 forceUpdate
* Should the complete set of users and groups be updated / created locally or just those known to have
* changed since the last sync? When <code>true</code> then <i>all</i> users and groups are queried from
* the user registry and updated locally. 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.
* @param isFullSync
* Should a complete set of user and group IDs be queried from the user registries in order to determine
* deletions? This parameter is independent of <code>force</code> as a separate query is run to process
* updates.
* @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 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 user or 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 user or group is invalid for the current authentication chain and whether the user
* or group needs to be 're-zoned'.
*/
private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate, boolean isFullSync, 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;
// Batch Process Names
final String[] reservedBatchProcessNames = { SyncProcess.GROUP_ANALYSIS.getTitle(zone), SyncProcess.USER_CREATION.getTitle(zone), SyncProcess.MISSING_AUTHORITY.getTitle(zone), SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone), SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone), SyncProcess.PERSON_ASSOCIATION.getTitle(zone), SyncProcess.AUTHORITY_DELETION.getTitle(zone) };
notifySyncDirectoryStart(zone, reservedBatchProcessNames);
// Ensure that the zoneId exists before multiple threads start using it
this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
@Override
public Void execute() throws Throwable {
authorityService.getOrCreateZone(zoneId);
return null;
}
}, false, splitTxns);
// The set of zones we associate with new objects (default plus registry specific)
final Set<String> zoneSet = getZones(zoneId);
long lastModifiedMillis = forceUpdate ? -1 : getMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, splitTxns);
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 + "'");
}
}
// First, analyze the group structure. Create maps of authorities to their parents for associations to create
// and delete. Also deal with 'overlaps' with other zones in the authentication chain.
final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(SyncProcess.GROUP_ANALYSIS.getTitle(zone), this.transactionService.getRetryingTransactionHelper(), userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, this.loggingInterval);
class Analyzer extends BaseBatchProcessWorker<NodeDescription> {
private final Map<String, String> groupsToCreate = new TreeMap<String, String>();
private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap();
private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap();
private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>();
private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>();
private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>();
private List<String> personsProcessed = new LinkedList<String>();
private Set<String> allZonePersons = Collections.emptySet();
private Set<String> deletionCandidates;
private long latestTime;
public Analyzer(final long latestTime) {
this.latestTime = latestTime;
}
public long getLatestTime() {
return this.latestTime;
}
public Set<String> getDeletionCandidates() {
return this.deletionCandidates;
}
public String getIdentifier(NodeDescription entry) {
return entry.getSourceId();
}
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
updateGroup(group, false);
} else {
// Check whether the group is in any of the authentication chain zones
Set<String> intersection = new TreeSet<String>(groupZones);
intersection.retainAll(allZoneIds);
// Check whether the group is in any of the higher priority authentication chain zones
Set<String> visited = new TreeSet<String>(intersection);
visited.retainAll(visitedZoneIds);
if (groupZones.contains(zoneId)) {
// The group already existed in this zone: update the group
updateGroup(group, true);
} else if (!visited.isEmpty()) {
// A group that exists in a different zone with higher precedence
return;
} else if (!allowDeletions || intersection.isEmpty()) {
// 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 + "'.");
}
updateAuthorityZones(groupName, groupZones, zoneSet);
// The group now exists in this zone: update the group
updateGroup(group, true);
} else {
// 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
updateGroup(group, false);
}
}
synchronized (this) {
// Maintain the last modified date
Date groupLastModified = group.getLastModified();
if (groupLastModified != null) {
this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
}
}
}
// Recursively walks and caches the authorities relating to and from this group so that we can later detect potential cycles
private Set<String> getContainedAuthorities(String groupName) {
// Return the cached children if it is processed
Set<String> children = this.finalGroupChildAssocs.get(groupName);
if (children != null) {
return children;
}
// First, recurse to the parent most authorities
for (String parent : ChainingUserRegistrySynchronizer.this.authorityService.getContainingAuthorities(null, groupName, true)) {
getContainedAuthorities(parent);
}
// Now descend on unprocessed parents.
return cacheContainedAuthorities(groupName);
}
private Set<String> cacheContainedAuthorities(String groupName) {
// Return the cached children if it is processed
Set<String> children = this.finalGroupChildAssocs.get(groupName);
if (children != null) {
return children;
}
// Descend on unprocessed parents.
children = ChainingUserRegistrySynchronizer.this.authorityService.getContainedAuthorities(null, groupName, true);
this.finalGroupChildAssocs.put(groupName, children);
for (String child : children) {
if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) {
cacheContainedAuthorities(child);
}
}
return children;
}
private synchronized void updateGroup(NodeDescription group, boolean existed) {
PropertyMap groupProperties = group.getProperties();
String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
if (groupDisplayName == null) {
groupDisplayName = ChainingUserRegistrySynchronizer.this.authorityService.getShortName(groupName);
}
// Divide the child associations into person and group associations, dealing with case sensitivity
Set<String> newChildPersons = newPersonSet();
Set<String> newChildGroups = new TreeSet<String>();
for (String child : group.getChildAssociations()) {
if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
newChildPersons.add(child);
} else {
newChildGroups.add(child);
}
}
// Account for differences if already existing
if (existed) {
// Update the display name now
ChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName, groupDisplayName);
// Work out the association differences
for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) {
if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
if (!newChildPersons.remove(child)) {
recordParentAssociationDeletion(child, groupName);
}
} else {
if (!newChildGroups.remove(child)) {
recordParentAssociationDeletion(child, groupName);
}
}
}
} else // Mark as created if new
{
// Make sure each group to be created features in the association deletion map (as these are handled in the same phase)
recordParentAssociationDeletion(groupName, null);
this.groupsToCreate.put(groupName, groupDisplayName);
}
// Create new associations
for (String child : newChildPersons) {
// Make sure each person with association changes features as a key in the deletion map
recordParentAssociationDeletion(child, null);
recordParentAssociationCreation(child, groupName);
}
for (String child : newChildGroups) {
// Make sure each group with association changes features as a key in the deletion map
recordParentAssociationDeletion(child, null);
recordParentAssociationCreation(child, groupName);
}
}
private void recordParentAssociationDeletion(String child, String parent) {
Map<String, Set<String>> parentAssocs;
if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
parentAssocs = this.personParentAssocsToDelete;
} else {
// Reflect the change in the map of final group associations (for cycle detection later)
parentAssocs = this.groupParentAssocsToDelete;
if (parent != null) {
Set<String> children = this.finalGroupChildAssocs.get(parent);
children.remove(child);
}
}
Set<String> parents = parentAssocs.get(child);
if (parents == null) {
parents = new TreeSet<String>();
parentAssocs.put(child, parents);
}
if (parent != null) {
parents.add(parent);
}
}
private void recordParentAssociationCreation(String child, String parent) {
Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER ? this.personParentAssocsToCreate : this.groupParentAssocsToCreate;
Set<String> parents = parentAssocs.get(child);
if (parents == null) {
parents = new TreeSet<String>();
parentAssocs.put(child, parents);
}
if (parent != null) {
parents.add(parent);
}
}
private void validateGroupParentAssocsToCreate() {
Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<String, Set<String>> entry = i.next();
String group = entry.getKey();
Set<String> parents = entry.getValue();
Deque<String> visited = new LinkedList<String>();
Iterator<String> j = parents.iterator();
while (j.hasNext()) {
String parent = j.next();
visited.add(parent);
if (validateAuthorityChildren(visited, group)) {
// The association validated - commit it
Set<String> children = finalGroupChildAssocs.get(parent);
if (children == null) {
children = new TreeSet<String>();
finalGroupChildAssocs.put(parent, children);
}
children.add(group);
} else {
// The association did not validate - prune it out
if (logger.isWarnEnabled()) {
ChainingUserRegistrySynchronizer.logger.warn("Not adding group '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(group) + "' to group '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(parent) + "' as this creates a cyclic relationship");
}
j.remove();
}
visited.removeLast();
}
if (parents.isEmpty()) {
i.remove();
}
}
// Sort the group associations in parent-first order (root groups first) to minimize reindexing overhead
Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>(this.groupParentAssocsToCreate.size() * 2);
Deque<String> visited = new LinkedList<String>();
for (String authority : this.groupParentAssocsToCreate.keySet()) {
visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate, sortedGroupAssociations);
}
this.groupParentAssocsToCreate = sortedGroupAssociations;
}
private boolean validateAuthorityChildren(Deque<String> visited, String authority) {
if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
return true;
}
if (visited.contains(authority)) {
return false;
}
visited.add(authority);
try {
Set<String> children = this.finalGroupChildAssocs.get(authority);
if (children != null) {
for (String child : children) {
if (!validateAuthorityChildren(visited, child)) {
return false;
}
}
}
return true;
} finally {
visited.removeLast();
}
}
/**
* Visits the given authority by recursively visiting its parents in associationsOld and then adding the
* authority to associationsNew. Used to sort associationsOld into 'parent-first' order to minimize
* reindexing overhead.
*
* @param visited
* The ancestors that form the path to the authority to visit. Allows detection of cyclic child
* associations.
* @param authority
* the authority to visit
* @param associationsOld
* the association map to sort
* @param associationsNew
* the association map to add to in parent-first order
*/
private boolean visitGroupParentAssocs(Deque<String> visited, String authority, Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) {
if (visited.contains(authority)) {
// Prevent cyclic paths (Shouldn't happen as we've already validated)
return false;
}
visited.add(authority);
try {
if (!associationsNew.containsKey(authority)) {
Set<String> oldParents = associationsOld.get(authority);
if (oldParents != null) {
Set<String> newParents = new TreeSet<String>();
for (String parent : oldParents) {
if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) {
newParents.add(parent);
}
}
associationsNew.put(authority, newParents);
}
}
return true;
} finally {
visited.removeLast();
}
}
private Set<String> newPersonSet() {
return ChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive() ? new TreeSet<String>() : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
}
private Map<String, Set<String>> newPersonMap() {
return ChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive() ? new TreeMap<String, Set<String>>() : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER);
}
private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) {
Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator();
StringBuilder groupList = null;
while (i.hasNext()) {
Map.Entry<String, Set<String>> entry = i.next();
String child = entry.getKey();
if (!toRetain.contains(child)) {
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
if (groupList == null) {
groupList = new StringBuilder(1024);
} else {
groupList.setLength(0);
}
for (String parent : entry.getValue()) {
if (groupList.length() > 0) {
groupList.append(", ");
}
groupList.append('\'').append(ChainingUserRegistrySynchronizer.this.authorityService.getShortName(parent)).append('\'');
}
ChainingUserRegistrySynchronizer.logger.debug("Ignoring non-existent member '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child) + "' in groups {" + groupList.toString() + "}");
}
i.remove();
}
}
}
private void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) {
// MNT-12454 fix. If syncDelete is false, there is no need to pull all users and all groups from LDAP during the full synchronization.
if ((syncDelete || !groupsToCreate.isEmpty()) && (isFullSync || !this.groupParentAssocsToDelete.isEmpty())) {
final Set<String> allZonePersons = newPersonSet();
final Set<String> allZoneGroups = new TreeSet<String>();
// Add in current set of known authorities
ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {
public Void execute() throws Throwable {
allZonePersons.addAll(ChainingUserRegistrySynchronizer.this.authorityService.getAllAuthoritiesInZone(zoneId, AuthorityType.USER));
allZoneGroups.addAll(ChainingUserRegistrySynchronizer.this.authorityService.getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP));
return null;
}
}, true, splitTxns);
allZoneGroups.addAll(this.groupsToCreate.keySet());
// Prune our set of authorities according to deletions
if (isFullSync) {
final Set<String> personDeletionCandidates = newPersonSet();
personDeletionCandidates.addAll(allZonePersons);
final Set<String> groupDeletionCandidates = new TreeSet<String>();
groupDeletionCandidates.addAll(allZoneGroups);
this.deletionCandidates = new TreeSet<String>();
for (String person : userRegistry.getPersonNames()) {
personDeletionCandidates.remove(person);
}
for (String group : userRegistry.getGroupNames()) {
groupDeletionCandidates.remove(group);
}
this.deletionCandidates = new TreeSet<String>();
this.deletionCandidates.addAll(personDeletionCandidates);
this.deletionCandidates.addAll(groupDeletionCandidates);
if (allowDeletions) {
allZonePersons.removeAll(personDeletionCandidates);
allZoneGroups.removeAll(groupDeletionCandidates);
} else {
// Complete association deletion information by scanning deleted groups
BatchProcessor<String> groupScanner = new BatchProcessor<String>(zone + " Missing Authority Scanning", ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(), this.deletionCandidates, ChainingUserRegistrySynchronizer.this.workerThreads, 20, ChainingUserRegistrySynchronizer.this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, ChainingUserRegistrySynchronizer.this.loggingInterval);
groupScanner.process(new BaseBatchProcessWorker<String>() {
@Override
public String getIdentifier(String entry) {
return entry;
}
@Override
public void process(String authority) throws Throwable {
// MNT-12454 fix. Modifies an authority's zone. Move authority from AUTH.EXT.LDAP1 to AUTH.ALF.
updateAuthorityZones(authority, Collections.singleton(zoneId), Collections.singleton(AuthorityService.ZONE_AUTH_ALFRESCO));
}
}, splitTxns);
}
}
// Prune the group associations now that we have complete information
this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups);
logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups);
this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups);
// Pruning person associations will have to wait until we have passed over all persons and built up
// this set
this.allZonePersons = allZonePersons;
if (!this.groupParentAssocsToDelete.isEmpty()) {
// Create/update the groups and delete parent associations to be deleted
// Batch 4 Group Creation and Association Deletion
BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone), ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(), this.groupParentAssocsToDelete.entrySet(), ChainingUserRegistrySynchronizer.this.workerThreads, 20, ChainingUserRegistrySynchronizer.this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, ChainingUserRegistrySynchronizer.this.loggingInterval);
groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
public String getIdentifier(Map.Entry<String, Set<String>> entry) {
return entry.getKey() + " " + entry.getValue();
}
public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
String child = entry.getKey();
String groupDisplayName = Analyzer.this.groupsToCreate.get(child);
if (groupDisplayName != null) {
String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child);
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
ChainingUserRegistrySynchronizer.logger.debug("Creating group '" + groupShortName + "'");
}
// create the group
ChainingUserRegistrySynchronizer.this.authorityService.createAuthority(AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName, zoneSet);
} else {
// Maintain association deletions now. The creations will have to be done later once
// we have performed all the deletions in order to avoid creating cycles
maintainAssociationDeletions(child);
}
}
}, splitTxns);
}
}
}
private void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) {
// First validate the group associations to be created for potential cycles. Remove any offending association
validateGroupParentAssocsToCreate();
// Now go ahead and create the group associations
if (!this.groupParentAssocsToCreate.isEmpty()) {
// Batch 5 Group Association Creation
BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone), ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(), this.groupParentAssocsToCreate.entrySet(), ChainingUserRegistrySynchronizer.this.workerThreads, 20, ChainingUserRegistrySynchronizer.this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, ChainingUserRegistrySynchronizer.this.loggingInterval);
groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
public String getIdentifier(Map.Entry<String, Set<String>> entry) {
return entry.getKey() + " " + entry.getValue();
}
public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
maintainAssociationCreations(entry.getKey());
}
}, splitTxns);
}
// Remove all the associations we have already dealt with
this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed);
// Filter out associations to authorities that simply can't exist (and log if debugging is enabled)
logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons);
// Update associations to persons not updated themselves
if (!this.personParentAssocsToDelete.isEmpty()) {
// Batch 6 Person Association
BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(SyncProcess.PERSON_ASSOCIATION.getTitle(zone), ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(), this.personParentAssocsToDelete.entrySet(), ChainingUserRegistrySynchronizer.this.workerThreads, 20, ChainingUserRegistrySynchronizer.this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, ChainingUserRegistrySynchronizer.this.loggingInterval);
groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
public String getIdentifier(Map.Entry<String, Set<String>> entry) {
return entry.getKey() + " " + entry.getValue();
}
public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
maintainAssociationDeletions(entry.getKey());
maintainAssociationCreations(entry.getKey());
}
}, splitTxns);
}
}
private void maintainAssociationDeletions(String authorityName) {
boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName) : this.groupParentAssocsToDelete.get(authorityName);
if (parentsToDelete != null && !parentsToDelete.isEmpty()) {
for (String parent : parentsToDelete) {
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
ChainingUserRegistrySynchronizer.logger.debug("Removing '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(authorityName) + "' from group '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(parent) + "'");
}
ChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent, authorityName);
}
}
}
private void maintainAssociationCreations(String authorityName) {
boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName) : this.groupParentAssocsToCreate.get(authorityName);
if (parents != null && !parents.isEmpty()) {
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
for (String groupName : parents) {
ChainingUserRegistrySynchronizer.logger.debug("Adding '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(authorityName) + "' to group '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(groupName) + "'");
}
}
try {
ChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents, authorityName);
} catch (UnknownAuthorityException e) {
// waiting for another worker thread to create it
throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e);
} catch (InvalidNodeRefException e) {
// See: ALF-5471: 'authorityMigration' patch can report 'Node does not exist'
throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e);
}
}
// Remember that this person's associations have been maintained
if (isPerson) {
synchronized (this) {
this.personsProcessed.add(authorityName);
}
}
}
}
// end of Analyzer class
// Run the first process the Group Analyzer
final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis);
int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns);
groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns);
// Process persons and their parent associations
lastModifiedMillis = forceUpdate ? -1 : getMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId, splitTxns);
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 + "'");
}
}
// User Creation and Association
final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(SyncProcess.USER_CREATION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(), userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, this.loggingInterval);
final UserRegistry userRegistryFinalRef = userRegistry;
class PersonWorker extends BaseBatchProcessWorker<NodeDescription> {
private long latestTime;
public PersonWorker(final long latestTime) {
this.latestTime = latestTime;
}
public long getLatestTime() {
return this.latestTime;
}
public String getIdentifier(NodeDescription entry) {
return entry.getSourceId();
}
public void process(NodeDescription person) throws Throwable {
// Make a mutable copy of the person properties, since they get written back to by person service
HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>(person.getProperties());
String personName = personProperties.get(ContentModel.PROP_USERNAME).toString().trim();
personProperties.put(ContentModel.PROP_USERNAME, personName);
if (Boolean.parseBoolean(ChainingUserRegistrySynchronizer.this.externalUserControl) && ChainingUserRegistrySynchronizer.this.externalUserControlSubsystemName.equals(zone) && userRegistryFinalRef instanceof LDAPUserRegistry) {
try {
LDAPUserRegistry ldapUserRegistry = (LDAPUserRegistry) userRegistryFinalRef;
if (ldapUserRegistry.getUserAccountStatusInterpreter() != null) {
QName propertyNameToCheck = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "userAccountStatusProperty");
if (personProperties.get(propertyNameToCheck) != null || ldapUserRegistry.getUserAccountStatusInterpreter().acceptsNullArgument()) {
boolean isUserAccountDisabled = ldapUserRegistry.getUserAccountStatusInterpreter().isUserAccountDisabled(personProperties.get(propertyNameToCheck));
personProperties.put(ContentModel.PROP_ENABLED, !isUserAccountDisabled);
}
}
} catch (IllegalArgumentException iae) {
// Can be thrown by certain implementations of AbstractDirectoryServiceUserAccountStatusInterpreter;
// We'll just log it.
ChainingUserRegistrySynchronizer.logger.debug(iae.getMessage(), iae);
}
}
// for invalid names will throw ConstraintException that will be catched by BatchProcessor$TxnCallback
nameChecker.evaluate(personName);
Set<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, false);
} else {
// Check whether the user is in any of the authentication chain zones
Set<String> intersection = new TreeSet<String>(zones);
intersection.retainAll(allZoneIds);
// Check whether the user is in any of the higher priority authentication chain zones
Set<String> visited = new TreeSet<String>(intersection);
visited.retainAll(visitedZoneIds);
if (visited.size() > 0) {
// A person that exists in a different zone with higher precedence - ignore
return;
} else if (!allowDeletions || intersection.isEmpty()) {
// not 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 + "'.");
}
updateAuthorityZones(personName, zones, zoneSet);
ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName, personProperties, false);
} else {
// 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);
}
}
// Maintain association deletions and creations in one shot (safe to do this with persons as we can't
// create cycles)
groupAnalyzer.maintainAssociationDeletions(personName);
groupAnalyzer.maintainAssociationCreations(personName);
synchronized (this) {
// Maintain the last modified date
Date personLastModified = person.getLastModified();
if (personLastModified != null) {
this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
}
}
}
}
PersonWorker persons = new PersonWorker(lastModifiedMillis);
int personProcessedCount = personProcessor.process(persons, splitTxns);
// Process those associations to persons who themselves have not been updated
groupAnalyzer.finalizeAssociations(userRegistry, splitTxns);
// Only now that the whole tree has been processed is it safe to persist the last modified dates
long latestTime = groupAnalyzer.getLatestTime();
if (latestTime != -1) {
setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, latestTime, splitTxns);
}
latestTime = persons.getLatestTime();
if (latestTime != -1) {
setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId, latestTime, splitTxns);
}
// Delete authorities if we have complete information for the zone
Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates();
if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) {
// Batch 7 Authority Deletion
BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(SyncProcess.AUTHORITY_DELETION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(), deletionCandidates, this.workerThreads, 10, this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, this.loggingInterval);
class AuthorityDeleter extends BaseBatchProcessWorker<String> {
private int personProcessedCount;
private int groupProcessedCount;
public int getPersonProcessedCount() {
return this.personProcessedCount;
}
public int getGroupProcessedCount() {
return this.groupProcessedCount;
}
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);
synchronized (this) {
this.personProcessedCount++;
}
} else {
if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
ChainingUserRegistrySynchronizer.logger.debug("Deleting group '" + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(authority) + "'");
}
ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
synchronized (this) {
this.groupProcessedCount++;
}
}
}
}
AuthorityDeleter authorityDeleter = new AuthorityDeleter();
authorityDeletionProcessor.process(authorityDeleter, splitTxns);
groupProcessedCount += authorityDeleter.getGroupProcessedCount();
personProcessedCount += authorityDeleter.getPersonProcessedCount();
}
// Remember we have visited this zone
visitedZoneIds.add(zoneId);
Object[] statusParams = { personProcessedCount, groupProcessedCount };
final String statusMessage = I18NUtil.getMessage("synchronization.summary.status", statusParams);
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
ChainingUserRegistrySynchronizer.logger.info("Finished synchronizing users and groups with user registry '" + zone + "'");
ChainingUserRegistrySynchronizer.logger.info(statusMessage);
}
notifySyncDirectoryEnd(zone, statusMessage);
}
use of org.alfresco.repo.security.sync.ldap.LDAPUserRegistry in project alfresco-repository by Alfresco.
the class LDAPUserRegistryTest method testTimeoutDuringSyncForCommunicationException.
/**
* Test for MNT-21614: Check & fail if communication breaks due to javax.naming.CommunicationException
*/
@Test
public void testTimeoutDuringSyncForCommunicationException() throws Exception {
LDAPUserRegistry userRegistry = createRegistry();
when(initialDirContext.getAttributes(eq(LDAPUserRegistry.jndiName(MEMBER_ATTRIBUTE_VALUE)), any())).thenThrow(new CommunicationException(" test."));
try {
userRegistry.getGroups(new Date());
fail("The process should fail with an exception");
} catch (AlfrescoRuntimeException are) {
assertEquals("The error message is not of the right format.", "synchronization.err.ldap.search", are.getMsgId());
assertTrue("The error message was not caused by timeout.", are.getCause().getMessage().contains(" test."));
}
}
use of org.alfresco.repo.security.sync.ldap.LDAPUserRegistry in project alfresco-repository by Alfresco.
the class LDAPUserRegistryTest method testTimeoutDuringSyncForServiceUnavailableException.
/**
* Test for MNT-21614: Check & fail if communication breaks due to javax.naming.ServiceUnavailableException
*/
@Test
public void testTimeoutDuringSyncForServiceUnavailableException() throws Exception {
LDAPUserRegistry userRegistry = createRegistry();
when(initialDirContext.getAttributes(eq(LDAPUserRegistry.jndiName(MEMBER_ATTRIBUTE_VALUE)), any())).thenThrow(new ServiceUnavailableException(" test."));
try {
userRegistry.getGroups(new Date());
fail("The process should fail with an exception");
} catch (AlfrescoRuntimeException are) {
assertEquals("The error message is not of the right format.", "synchronization.err.ldap.search", are.getMsgId());
assertTrue("The error message was not caused by timeout.", are.getCause().getMessage().contains(" test."));
}
}
use of org.alfresco.repo.security.sync.ldap.LDAPUserRegistry in project alfresco-repository by Alfresco.
the class LDAPUserRegistryTest method testTimeoutDuringSync.
/**
* Test for MNT-17966
*/
@Test
public void testTimeoutDuringSync() throws Exception {
LDAPUserRegistry userRegistry = createRegistry();
when(initialDirContext.getAttributes(eq(LDAPUserRegistry.jndiName(MEMBER_ATTRIBUTE_VALUE)), any())).thenThrow(new NamingException(LDAPUserRegistry.NAMING_TIMEOUT_EXCEPTION_MESSAGE + " test."));
try {
userRegistry.getGroups(new Date());
fail("The process should fail with an exception");
} catch (AlfrescoRuntimeException are) {
assertEquals("The error message is not of the right format.", "synchronization.err.ldap.search", are.getMsgId());
assertTrue("The error message was not caused by timeout.", are.getCause().getMessage().contains(LDAPUserRegistry.NAMING_TIMEOUT_EXCEPTION_MESSAGE));
}
}
Aggregations