Search in sources :

Example 1 with LDAPUserRegistry

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;
}
Also used : LDAPUserRegistry(org.alfresco.repo.security.sync.ldap.LDAPUserRegistry) HashSet(java.util.HashSet)

Example 2 with LDAPUserRegistry

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);
}
Also used : Serializable(java.io.Serializable) ConcurrencyFailureException(org.springframework.dao.ConcurrencyFailureException) InvalidNodeRefException(org.alfresco.service.cmr.repository.InvalidNodeRefException) UnknownAuthorityException(org.alfresco.repo.security.authority.UnknownAuthorityException) LDAPUserRegistry(org.alfresco.repo.security.sync.ldap.LDAPUserRegistry) PropertyMap(org.alfresco.util.PropertyMap) BatchProcessor(org.alfresco.repo.batch.BatchProcessor) LDAPUserRegistry(org.alfresco.repo.security.sync.ldap.LDAPUserRegistry) QName(org.alfresco.service.namespace.QName) PropertyMap(org.alfresco.util.PropertyMap)

Example 3 with LDAPUserRegistry

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."));
    }
}
Also used : CommunicationException(javax.naming.CommunicationException) LDAPUserRegistry(org.alfresco.repo.security.sync.ldap.LDAPUserRegistry) AlfrescoRuntimeException(org.alfresco.error.AlfrescoRuntimeException) Date(java.util.Date) Test(org.junit.Test)

Example 4 with LDAPUserRegistry

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."));
    }
}
Also used : LDAPUserRegistry(org.alfresco.repo.security.sync.ldap.LDAPUserRegistry) AlfrescoRuntimeException(org.alfresco.error.AlfrescoRuntimeException) ServiceUnavailableException(javax.naming.ServiceUnavailableException) Date(java.util.Date) Test(org.junit.Test)

Example 5 with LDAPUserRegistry

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));
    }
}
Also used : LDAPUserRegistry(org.alfresco.repo.security.sync.ldap.LDAPUserRegistry) AlfrescoRuntimeException(org.alfresco.error.AlfrescoRuntimeException) NamingException(javax.naming.NamingException) Date(java.util.Date) Test(org.junit.Test)

Aggregations

LDAPUserRegistry (org.alfresco.repo.security.sync.ldap.LDAPUserRegistry)5 Date (java.util.Date)3 AlfrescoRuntimeException (org.alfresco.error.AlfrescoRuntimeException)3 Test (org.junit.Test)3 Serializable (java.io.Serializable)1 HashSet (java.util.HashSet)1 CommunicationException (javax.naming.CommunicationException)1 NamingException (javax.naming.NamingException)1 ServiceUnavailableException (javax.naming.ServiceUnavailableException)1 BatchProcessor (org.alfresco.repo.batch.BatchProcessor)1 UnknownAuthorityException (org.alfresco.repo.security.authority.UnknownAuthorityException)1 InvalidNodeRefException (org.alfresco.service.cmr.repository.InvalidNodeRefException)1 QName (org.alfresco.service.namespace.QName)1 PropertyMap (org.alfresco.util.PropertyMap)1 ConcurrencyFailureException (org.springframework.dao.ConcurrencyFailureException)1