Merged V3.2 to HEAD

15636: ETHREEOH-2626: LDAP sync will no longer delete and recreate colliding users and groups in zones that aren't even in the authentication chain.
      - Instead such users and groups will be 're-zoned' to the first zone where they were found
      - Avoids losing site memberships, etc. on upgrade or change of authentication chain
      - Will continue to recreate users and groups from lower priority zones in the authentication chain
      - Updated unit tests appropriately


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@15637 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Dave Ward
2009-08-06 18:43:41 +00:00
parent 5de6533899
commit 8fa726f7df
2 changed files with 152 additions and 79 deletions

View File

@@ -25,6 +25,7 @@
package org.alfresco.repo.security.sync;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
@@ -231,7 +232,16 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl
public void synchronize(boolean force, boolean splitTxns)
{
Set<String> visitedZoneIds = new TreeSet<String>();
for (String id : this.applicationContextManager.getInstanceIds())
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
@@ -257,8 +267,10 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl
boolean requiresNew = splitTxns
|| AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
int personsProcessed = syncPersonsWithPlugin(id, plugin, force, requiresNew, visitedZoneIds);
int groupsProcessed = syncGroupsWithPlugin(id, plugin, force, requiresNew, visitedZoneIds);
int personsProcessed = syncPersonsWithPlugin(id, plugin, force, requiresNew, visitedZoneIds,
allZoneIds);
int groupsProcessed = syncGroupsWithPlugin(id, plugin, force, requiresNew, visitedZoneIds,
allZoneIds);
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
ChainingUserRegistrySynchronizer.logger
@@ -334,10 +346,14 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl
* 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(String zone, UserRegistry userRegistry, boolean force, boolean splitTxns,
final Set<String> visitedZoneIds)
private int syncPersonsWithPlugin(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;
@@ -408,23 +424,47 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl
}
else
{
// The person does not exist in this zone, but may exist in another zone
zones.retainAll(visitedZoneIds);
if (zones.size() > 0)
// 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)
{
// A person that exists in a different zone with higher precedence
continue;
// 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);
}
// The person existed, but in a zone with lower precedence
if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled())
else
{
ChainingUserRegistrySynchronizer.logger
.warn("Recreating occluded user '"
+ personName
+ "'. This user was previously created manually or through synchronization with a lower priority user registry.");
// 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
continue;
}
// 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);
}
ChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet);
}
// Increment the count of processed people
personsCreated.add(personName);
@@ -513,10 +553,14 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl
* 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(String zone, UserRegistry userRegistry, boolean force, boolean splitTxns,
final Set<String> visitedZoneIds)
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;
@@ -566,14 +610,14 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl
NodeDescription group = groups.next();
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
String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService
.getShortName(groupName);
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
ChainingUserRegistrySynchronizer.logger.info("Creating group '" + groupShortName + "'");
@@ -588,64 +632,81 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl
groupAssocsToCreate.put(groupName, children);
}
}
else if (groupZones.contains(zoneId))
{
// The group already existed in this 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())
{
groupAssocsToCreate.put(groupName, toAdd);
}
for (String child : toDelete)
{
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
ChainingUserRegistrySynchronizer.logger.info("Removing '"
+ ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child)
+ "' from group '"
+ ChainingUserRegistrySynchronizer.this.authorityService
.getShortName(groupName) + "'");
}
ChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(groupName, child);
}
}
else
{
// The group does not exist in this zone, but may exist in another zone
groupZones.retainAll(visitedZoneIds);
if (groupZones.size() > 0)
// 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())
{
// A group that exists in a different zone with higher precedence
continue;
// 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);
}
String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService
.getShortName(groupName);
// The group existed, but in a zone with lower precedence
if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled())
if (groupZones.contains(zoneId) || intersection.isEmpty())
{
ChainingUserRegistrySynchronizer.logger
.warn("Recreating occluded group '"
+ groupShortName
+ "'. This group was previously created manually or through synchronization with a lower priority user registry.");
// 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())
{
groupAssocsToCreate.put(groupName, toAdd);
}
for (String child : toDelete)
{
if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled())
{
ChainingUserRegistrySynchronizer.logger.info("Removing '"
+ ChainingUserRegistrySynchronizer.this.authorityService
.getShortName(child) + "' from group '" + groupShortName + "'");
}
ChainingUserRegistrySynchronizer.this.authorityService
.removeAuthority(groupName, child);
}
}
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())
else
{
groupAssocsToCreate.put(groupName, children);
// 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
continue;
}
// 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())
{
groupAssocsToCreate.put(groupName, children);
}
}
}
// Increment the count of processed groups
processedCount++;
groupsCreated.add(groupName);

View File

@@ -117,12 +117,17 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase
}
/**
* 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
* Sets up the test users and groups in three zones, "Z0", "Z1" and "Z2", by doing a forced synchronize with a Mock
* user registry. Note that the zones have some overlapping entries. "Z0" is not used in subsequent synchronizations
* and is used to test that users and groups in zones that aren't in the authentication chain get 're-zoned'
* appropriately. The layout is as follows
*
* <pre>
* Z1
* Z0
* G1
* U6
*
* Z1
* G2 - U1, G3 - U2, G4, G5
*
* Z2
@@ -135,13 +140,18 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase
*/
private void setUpTestUsersAndGroups() throws Exception
{
this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[]
this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", new NodeDescription[]
{
newPerson("U6")
}, new NodeDescription[]
{
newGroup("G1")
}), 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")
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")
@@ -163,9 +173,10 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase
public Object execute() throws Throwable
{
assertExists("Z0", "U6");
assertExists("Z0", "G1");
assertExists("Z1", "U1");
assertExists("Z1", "U2");
assertExists("Z1", "G1");
assertExists("Z1", "G2", "U1", "G3");
assertExists("Z1", "G3", "U2", "G4", "G5");
assertExists("Z1", "G4");
@@ -183,7 +194,8 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase
private void tearDownTestUsersAndGroups() throws Exception
{
// Wipe out everything that was in Z1 and Z2
this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[] {},
this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", new NodeDescription[] {},
new NodeDescription[] {}), new MockUserRegistry("Z1", new NodeDescription[] {},
new NodeDescription[] {}), new MockUserRegistry("Z2", new NodeDescription[] {},
new NodeDescription[] {}));
this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback<Object>()