diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 978c51393b..14d1875a70 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -237,12 +237,51 @@ ${db.pool.max} + + ${db.pool.min} + + + ${db.pool.idle} + false ${db.txn.isolation} + + ${db.pool.wait.max} + + + ${db.pool.validate.query} + + + ${db.pool.evict.interval} + + + ${db.pool.evict.idle.min} + + + ${db.pool.validate.borrow} + + + ${db.pool.validate.return} + + + ${db.pool.evict.validate} + + + ${db.pool.abandoned.detect} + + + ${db.pool.abandoned.time} + + + ${db.pool.statements.enable} + + + ${db.pool.statements.max} + diff --git a/config/alfresco/extension/custom-connection-pool-context.xml.sample b/config/alfresco/extension/custom-connection-pool-context.xml.sample deleted file mode 100644 index 03d37b4cf3..0000000000 --- a/config/alfresco/extension/custom-connection-pool-context.xml.sample +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - ${db.driver} - - - ${db.url} - - - ${db.username} - - - ${db.password} - - - false - - - - ${db.pool.initial} - - - ${db.pool.max} - - - 10000 - - - select 1 - - - 300000 - - - 60000 - - - false - - - false - - - true - - - true - - - 30 - - - - diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index ed9c7d8924..bf8f6693cd 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -190,6 +190,19 @@ db.password=alfresco db.pool.initial=10 db.pool.max=40 db.txn.isolation=-1 +db.pool.statements.enable=true +db.pool.statements.max=40 +db.pool.min=0 +db.pool.idle=8 +db.pool.wait.max=-1 +db.pool.validate.query= +db.pool.evict.interval=-1 +db.pool.evict.idle.min=1800000 +db.pool.validate.borrow=true +db.pool.validate.return=false +db.pool.evict.validate=false +db.pool.abandoned.detect=false +db.pool.abandoned.time=300 # Email configuration mail.host= diff --git a/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml b/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml index 19f5670947..f67dffe3d3 100644 --- a/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml +++ b/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml @@ -35,6 +35,9 @@ ${synchronization.syncWhenMissingPeopleLogIn} + + ${synchronization.syncOnStartup} + ${synchronization.autoCreatePeopleOnLogin} @@ -50,6 +53,9 @@ + + + userRegistry diff --git a/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties b/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties index 56ec2d23b3..81ae75786c 100644 --- a/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties +++ b/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties @@ -15,5 +15,8 @@ synchronization.import.cron=0 0 0 * * ? # Should we trigger a differential sync when missing people log in? synchronization.syncWhenMissingPeopleLogIn=true +# Should we trigger a differential sync on startup? +synchronization.syncOnStartup=true + # Should we auto create a missing person on log in? synchronization.autoCreatePeopleOnLogin=true \ No newline at end of file diff --git a/source/java/org/alfresco/repo/domain/AccessControlListDAO.java b/source/java/org/alfresco/repo/domain/AccessControlListDAO.java index 96b31479ab..86394e415c 100644 --- a/source/java/org/alfresco/repo/domain/AccessControlListDAO.java +++ b/source/java/org/alfresco/repo/domain/AccessControlListDAO.java @@ -83,11 +83,11 @@ public interface AccessControlListDAO * Update inheritance * * @param parent - * @param mergeFrom + * @param inheritFrom * @param previousId * @return */ - public List setInheritanceForChildren(NodeRef parent, Long mergeFrom); + public List setInheritanceForChildren(NodeRef parent, Long inheritFrom); public Long getIndirectAcl(NodeRef nodeRef); diff --git a/source/java/org/alfresco/repo/domain/hibernate/AVMAccessControlListDAO.java b/source/java/org/alfresco/repo/domain/hibernate/AVMAccessControlListDAO.java index bdc8eea9f3..507dcc2b9c 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/AVMAccessControlListDAO.java +++ b/source/java/org/alfresco/repo/domain/hibernate/AVMAccessControlListDAO.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2007 Alfresco Software Limited. + * Copyright (C) 2005-2009 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -14,12 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * As a special exception to the terms and conditions of version 2.0 of - * the GPL, you may redistribute this Program in connection with Free/Libre - * and Open Source Software ("FLOSS") applications as described in Alfresco's - * FLOSS exception. You should have recieved a copy of the text describing - * the FLOSS exception, and it is also available here: - * http://www.alfresco.com/legal/licensing" */ + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ package org.alfresco.repo.domain.hibernate; @@ -38,7 +40,6 @@ import org.alfresco.repo.domain.PropertyValue; import org.alfresco.repo.domain.hibernate.AclDaoComponentImpl.Indirection; import org.alfresco.repo.search.AVMSnapShotTriggeredIndexingMethodInterceptor; import org.alfresco.repo.search.AVMSnapShotTriggeredIndexingMethodInterceptor.StoreType; -import org.alfresco.repo.search.impl.lucene.index.IndexInfo; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.permissions.ACLType; @@ -509,7 +510,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO if (descriptor.isLayeredDirectory()) { - setInheritanceForDirectChildren(descriptor, changeMap, aclDaoComponent.getInheritedAccessControlList(getAclAsSystem(-1, descriptor.getPath()).getId()), + setInheritanceForDirectChildren(descriptor, changeMap, getAclAsSystem(-1, descriptor.getPath()).getId(), indirections); } fixUpAcls(descriptor, changeMap, unchanged, unsetAcl, mode, indirections); @@ -586,10 +587,10 @@ public class AVMAccessControlListDAO implements AccessControlListDAO } - private void setInheritanceForDirectChildren(AVMNodeDescriptor descriptor, Map changeMap, Long mergeFrom, Map> indirections) + private void setInheritanceForDirectChildren(AVMNodeDescriptor descriptor, Map changeMap, Long inheritFrom, Map> indirections) { List changes = new ArrayList(); - setFixedAcls(descriptor, mergeFrom, changes, SetMode.DIRECT_ONLY, false, indirections); + setFixedAcls(descriptor, inheritFrom, null, changes, SetMode.DIRECT_ONLY, false, indirections); for (AclChange change : changes) { if (!change.getBefore().equals(change.getAfter())) @@ -599,7 +600,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO } } - public List setInheritanceForChildren(NodeRef parent, Long mergeFrom) + public List setInheritanceForChildren(NodeRef parent, Long inheritFrom) { // Walk children and fix up any that reference the given list .. // If previous is null we need to visit all descendants with a null acl and set @@ -615,7 +616,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO Map> indirections = buildIndirections(); List changes = new ArrayList(); AVMNodeDescriptor descriptor = fAVMService.lookup(version, path); - setFixedAcls(descriptor, mergeFrom, changes, SetMode.ALL, false, indirections); + setFixedAcls(descriptor, inheritFrom, null, changes, SetMode.ALL, false, indirections); return changes; } @@ -626,16 +627,24 @@ public class AVMAccessControlListDAO implements AccessControlListDAO } /** - * Set and cascade ACls + * Support to set a shared ACL on a node and all of its children. * * @param descriptor + * the descriptor + * @param inheritFrom + * the parent node's ACL * @param mergeFrom + * the shared ACL, if already known. If null, will be retrieved / created lazily * @param changes + * the list in which to record changes * @param mode + * the mode * @param set + * set the shared ACL on the parent ? * @param indirections + * the indirections */ - public void setFixedAcls(AVMNodeDescriptor descriptor, Long mergeFrom, List changes, SetMode mode, boolean set, Map> indirections) + public void setFixedAcls(AVMNodeDescriptor descriptor, Long inheritFrom, Long mergeFrom, List changes, SetMode mode, boolean set, Map> indirections) { if (descriptor == null) { @@ -645,6 +654,12 @@ public class AVMAccessControlListDAO implements AccessControlListDAO { if (set) { + // Lazily retrieve/create the shared ACL + if (mergeFrom == null) + { + mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom); + } + // Simple set does not require any special COW wire up // The AVM node will COW as required DbAccessControlList previous = getAclAsSystem(-1, descriptor.getPath()); @@ -673,6 +688,12 @@ public class AVMAccessControlListDAO implements AccessControlListDAO for (String key : children.keySet()) { + // Lazily retrieve/create the shared ACL + if (mergeFrom == null) + { + mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom); + } + AVMNodeDescriptor child = children.get(key); DbAccessControlList acl = getAclAsSystem(-1, child.getPath()); @@ -682,7 +703,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO hibernateSessionHelper.mark(); try { - setFixedAcls(child, mergeFrom, changes, mode, true, indirections); + setFixedAcls(child, inheritFrom, mergeFrom, changes, mode, true, indirections); } finally { @@ -709,7 +730,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO try { setAclAsSystem(child.getPath(), aclDaoComponent.getDbAccessControlList(change.getAfter())); - setFixedAcls(child, aclDaoComponent.getInheritedAccessControlList(change.getAfter()), newChanges, SetMode.DIRECT_ONLY, false, indirections); + setFixedAcls(child, change.getAfter(), null, newChanges, SetMode.DIRECT_ONLY, false, indirections); changes.addAll(newChanges); break; } @@ -725,7 +746,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO hibernateSessionHelper.mark(); try { - setFixedAcls(child, mergeFrom, changes, mode, true, indirections); + setFixedAcls(child, inheritFrom, mergeFrom, changes, mode, true, indirections); } finally { @@ -863,7 +884,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO List changes = new ArrayList(); - setFixedAcls(node, aclDaoComponent.getInheritedAccessControlList(id), changes, SetMode.DIRECT_ONLY, false, indirections); + setFixedAcls(node, id, null, changes, SetMode.DIRECT_ONLY, false, indirections); for (AclChange change : changes) { @@ -909,7 +930,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO } List changes = new ArrayList(); - setFixedAcls(node, aclDaoComponent.getInheritedAccessControlList(getAclAsSystem(-1, node.getPath()).getId()), changes, SetMode.DIRECT_ONLY, false, indirections); + setFixedAcls(node, getAclAsSystem(-1, node.getPath()).getId(), null, changes, SetMode.DIRECT_ONLY, false, indirections); for (AclChange change : changes) { @@ -949,7 +970,7 @@ public class AVMAccessControlListDAO implements AccessControlListDAO } List changes = new ArrayList(); - setFixedAcls(node, aclDaoComponent.getInheritedAccessControlList(getAclAsSystem(-1, node.getPath()).getId()), changes, SetMode.DIRECT_ONLY, false, indirections); + setFixedAcls(node, getAclAsSystem(-1, node.getPath()).getId(), null, changes, SetMode.DIRECT_ONLY, false, indirections); for (AclChange change : changes) { diff --git a/source/java/org/alfresco/repo/domain/hibernate/DMAccessControlListDAO.java b/source/java/org/alfresco/repo/domain/hibernate/DMAccessControlListDAO.java index 3cf8bef009..fa4c0d3601 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/DMAccessControlListDAO.java +++ b/source/java/org/alfresco/repo/domain/hibernate/DMAccessControlListDAO.java @@ -170,7 +170,6 @@ public class DMAccessControlListDAO implements AccessControlListDAO { if (!store.getProtocol().equals(StoreRef.PROTOCOL_AVM)) { - @SuppressWarnings("unused") CounterSet update; update = fixOldDmAcls(nodeService.getRootNode(store), null, true); result.add(update); @@ -316,10 +315,10 @@ public class DMAccessControlListDAO implements AccessControlListDAO throw new UnsupportedOperationException(); } - public List setInheritanceForChildren(NodeRef parent, Long mergeFrom) + public List setInheritanceForChildren(NodeRef parent, Long inheritFrom) { List changes = new ArrayList(); - setFixedAcls(parent, mergeFrom, changes, false); + setFixedAcls(parent, inheritFrom, null, changes, false); return changes; } @@ -329,14 +328,20 @@ public class DMAccessControlListDAO implements AccessControlListDAO } /** - * Support to set ACLs and cascade fo required + * Support to set a shared ACL on a node and all of its children * * @param nodeRef + * the parent node + * @param inheritFrom + * the parent node's ACL * @param mergeFrom + * the shared ACL, if already known. If null, will be retrieved / created lazily * @param changes + * the list in which to record changes * @param set + * set the shared ACL on the parent ? */ - public void setFixedAcls(NodeRef nodeRef, Long mergeFrom, List changes, boolean set) + public void setFixedAcls(NodeRef nodeRef, Long inheritFrom, Long mergeFrom, List changes, boolean set) { if (nodeRef == null) { @@ -346,6 +351,11 @@ public class DMAccessControlListDAO implements AccessControlListDAO { if (set) { + // Lazily retrieve/create the shared ACL + if (mergeFrom == null) + { + mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom); + } setAccessControlList(nodeRef, aclDaoComponent.getDbAccessControlList(mergeFrom)); } @@ -355,6 +365,12 @@ public class DMAccessControlListDAO implements AccessControlListDAO { if (child.isPrimary()) { + // Lazily retrieve/create the shared ACL + if (mergeFrom == null) + { + mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom); + } + DbAccessControlList acl = getAccessControlList(child.getChildRef()); if (acl == null) @@ -362,7 +378,7 @@ public class DMAccessControlListDAO implements AccessControlListDAO hibernateSessionHelper.mark(); try { - setFixedAcls(child.getChildRef(), mergeFrom, changes, true); + setFixedAcls(child.getChildRef(), inheritFrom, mergeFrom, changes, true); } finally { @@ -386,7 +402,7 @@ public class DMAccessControlListDAO implements AccessControlListDAO hibernateSessionHelper.mark(); try { - setFixedAcls(child.getChildRef(), mergeFrom, changes, true); + setFixedAcls(child.getChildRef(), inheritFrom, mergeFrom, changes, true); } finally { diff --git a/source/java/org/alfresco/repo/domain/hibernate/DMPermissionsDaoComponentImpl.java b/source/java/org/alfresco/repo/domain/hibernate/DMPermissionsDaoComponentImpl.java index b8c634ea5d..d18979aa3c 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/DMPermissionsDaoComponentImpl.java +++ b/source/java/org/alfresco/repo/domain/hibernate/DMPermissionsDaoComponentImpl.java @@ -62,7 +62,7 @@ public class DMPermissionsDaoComponentImpl extends AbstractPermissionsDaoCompone List changes = new ArrayList(); DbAccessControlList acl = aclDaoComponent.getDbAccessControlList(id); changes.add(new AclDaoComponentImpl.AclChangeImpl(null, id, null, acl.getAclType())); - changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, aclDaoComponent.getInheritedAccessControlList(id))); + changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, id)); getACLDAO(nodeRef).setAccessControlList(nodeRef, acl); return new CreationReport(acl, changes); } @@ -90,7 +90,7 @@ public class DMPermissionsDaoComponentImpl extends AbstractPermissionsDaoCompone changes.add(new AclDaoComponentImpl.AclChangeImpl(existing.getId(), id, existing.getAclType(), acl.getAclType())); changes.addAll(aclDaoComponent.mergeInheritedAccessControlList(existing.getId(), id)); // set this to inherit to children - changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, aclDaoComponent.getInheritedAccessControlList(id))); + changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, id)); getACLDAO(nodeRef).setAccessControlList(nodeRef, acl); return new CreationReport(acl, changes); @@ -123,7 +123,6 @@ public class DMPermissionsDaoComponentImpl extends AbstractPermissionsDaoCompone case DEFINING: if (acl.getInheritsFrom() != null) { - @SuppressWarnings("unused") Long deleted = acl.getId(); Long inheritsFrom = acl.getInheritsFrom(); getACLDAO(nodeRef).setAccessControlList(nodeRef, aclDaoComponent.getDbAccessControlList(inheritsFrom)); @@ -135,7 +134,6 @@ public class DMPermissionsDaoComponentImpl extends AbstractPermissionsDaoCompone else { // TODO: could just cear out existing - @SuppressWarnings("unused") Long deleted = acl.getId(); SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties(); properties = new SimpleAccessControlListProperties(); @@ -146,7 +144,7 @@ public class DMPermissionsDaoComponentImpl extends AbstractPermissionsDaoCompone Long id = aclDaoComponent.createAccessControlList(properties); getACLDAO(nodeRef).setAccessControlList(nodeRef, aclDaoComponent.getDbAccessControlList(id)); List changes = new ArrayList(); - changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, aclDaoComponent.getInheritedAccessControlList(id))); + changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, id)); getACLDAO(nodeRef).updateChangedAcls(nodeRef, changes); aclDaoComponent.deleteAccessControlList(acl.getId()); } diff --git a/source/java/org/alfresco/repo/domain/hibernate/NodeAccessControlListDAO.java b/source/java/org/alfresco/repo/domain/hibernate/NodeAccessControlListDAO.java index a1355854e3..e85bcff6ee 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/NodeAccessControlListDAO.java +++ b/source/java/org/alfresco/repo/domain/hibernate/NodeAccessControlListDAO.java @@ -102,7 +102,7 @@ public class NodeAccessControlListDAO extends HibernateDaoSupport implements Acc // Nothing to do here } - public List setInheritanceForChildren(NodeRef parent, Long mergeFrom) + public List setInheritanceForChildren(NodeRef parent, Long inheritFrom) { // Nothing to do here return Collections. emptyList(); diff --git a/source/java/org/alfresco/repo/domain/hibernate/PermissionsDaoComponentImpl.java b/source/java/org/alfresco/repo/domain/hibernate/PermissionsDaoComponentImpl.java index c072ef816b..65473bb5cc 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/PermissionsDaoComponentImpl.java +++ b/source/java/org/alfresco/repo/domain/hibernate/PermissionsDaoComponentImpl.java @@ -57,7 +57,7 @@ public class PermissionsDaoComponentImpl extends AbstractPermissionsDaoComponent List changes = new ArrayList(); DbAccessControlList acl = aclDaoComponent.getDbAccessControlList(id); changes.add(new AclDaoComponentImpl.AclChangeImpl(null, id, null, acl.getAclType())); - changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, aclDaoComponent.getInheritedAccessControlList(id))); + changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, id)); getACLDAO(nodeRef).setAccessControlList(nodeRef, acl); return new CreationReport(acl, changes); } @@ -85,7 +85,7 @@ public class PermissionsDaoComponentImpl extends AbstractPermissionsDaoComponent changes.add(new AclDaoComponentImpl.AclChangeImpl(existing.getId(), id, existing.getAclType(), acl.getAclType())); changes.addAll(aclDaoComponent.mergeInheritedAccessControlList(existing.getId(), id)); // set this to inherit to children - changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, aclDaoComponent.getInheritedAccessControlList(id))); + changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, id)); getACLDAO(nodeRef).setAccessControlList(nodeRef, acl); return new CreationReport(acl, changes); @@ -122,7 +122,7 @@ public class PermissionsDaoComponentImpl extends AbstractPermissionsDaoComponent changes.addAll(aclDaoComponent.mergeInheritedAccessControlList(inheritedAclId, id)); } // set this to inherit to children - changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, aclDaoComponent.getInheritedAccessControlList(id))); + changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, id)); getACLDAO(nodeRef).setAccessControlList(nodeRef, acl); return new CreationReport(acl, changes); @@ -173,7 +173,7 @@ public class PermissionsDaoComponentImpl extends AbstractPermissionsDaoComponent Long id = aclDaoComponent.createAccessControlList(properties); getACLDAO(nodeRef).setAccessControlList(nodeRef, aclDaoComponent.getDbAccessControlList(id)); List changes = new ArrayList(); - changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, aclDaoComponent.getInheritedAccessControlList(id))); + changes.addAll(getACLDAO(nodeRef).setInheritanceForChildren(nodeRef, id)); getACLDAO(nodeRef).updateChangedAcls(nodeRef, changes); aclDaoComponent.deleteAccessControlList(acl.getId()); } diff --git a/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java b/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java index c418c54dcf..e065eb7497 100644 --- a/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java +++ b/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java @@ -46,6 +46,8 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** * This class abstract the support required to set up and query the Acegi context for security enforcement. There are @@ -71,6 +73,8 @@ public abstract class AbstractAuthenticationComponent implements AuthenticationC private TransactionService transactionService; private UserRegistrySynchronizer userRegistrySynchronizer; + + private final Log logger = LogFactory.getLog(getClass()); public AbstractAuthenticationComponent() { @@ -134,14 +138,37 @@ public abstract class AbstractAuthenticationComponent implements AuthenticationC public void authenticate(String userName, char[] password) throws AuthenticationException { + if (logger.isDebugEnabled()) + { + logger.debug("Authenticating user \"" + userName + '"'); + } // Support guest login from the login screen if (isGuestUserName(userName)) { + if (logger.isDebugEnabled()) + { + logger.debug("User \"" + userName + "\" recognized as a guest user"); + } setGuestUserAsCurrentUser(getUserDomain(userName)); } else { - authenticateImpl(userName, password); + try + { + authenticateImpl(userName, password); + } + catch (RuntimeException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Failed to authenticate user \"" + userName + '"', e); + } + throw e; + } + } + if (logger.isDebugEnabled()) + { + logger.debug("User \"" + userName + "\" authenticated successfully"); } } @@ -225,11 +252,20 @@ public abstract class AbstractAuthenticationComponent implements AuthenticationC UserDetails ud = null; if (isGuestUserName(userName)) { + String tenantDomain = getUserDomain(userName); + if (logger.isDebugEnabled()) + { + logger.debug("Setting the current user to the guest user of tenant domain \"" + tenantDomain + '"'); + } GrantedAuthority[] gas = new GrantedAuthority[0]; - ud = new User(getGuestUserName(getUserDomain(userName)), "", true, true, true, true, gas); + ud = new User(getGuestUserName(tenantDomain), "", true, true, true, true, gas); } else { + if (logger.isDebugEnabled()) + { + logger.debug("Setting the current user to \"" + userName + '"'); + } ud = getUserDetails(userName); } return setUserDetails(ud); @@ -428,17 +464,27 @@ public abstract class AbstractAuthenticationComponent implements AuthenticationC { public String doWork() throws Exception { - if (personService.personExists(userName)|| userRegistrySynchronizer.createMissingPerson(userName)) + if (!personService.personExists(userName)) { - NodeRef userNode = personService.getPerson(userName); - if (userNode != null) + if (logger.isDebugEnabled()) { - // Get the person name and use that as the current user to line up with permission - // checks - return (String) nodeService.getProperty(userNode, ContentModel.PROP_USERNAME); + logger.debug("User \"" + userName + + "\" does not exist in Alfresco. Attempting to import / create the user."); } + if (!userRegistrySynchronizer.createMissingPerson(userName)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Failed to import / create user \"" + userName + '"'); + } + throw new AuthenticationException("User \"" + userName + + "\" does not exist in Alfresco"); + } } - throw new AuthenticationException("Person does not exist in Alfresco"); + NodeRef userNode = personService.getPerson(userName); + // Get the person name and use that as the current user to line up with permission + // checks + return (String) nodeService.getProperty(userNode, ContentModel.PROP_USERNAME); } }, getSystemUserName(getUserDomain(userName))); @@ -499,6 +545,10 @@ public abstract class AbstractAuthenticationComponent implements AuthenticationC public Authentication setSystemUserAsCurrentUser(String tenantDomain) { + if (logger.isDebugEnabled()) + { + logger.debug("Setting the current user to the system user of tenant domain \"" + tenantDomain + '"'); + } return authenticationContext.setSystemUserAsCurrentUser(tenantDomain); } diff --git a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java index 2ab205eecb..c8594113e2 100644 --- a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java +++ b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java @@ -40,15 +40,22 @@ import org.alfresco.repo.attributes.MapAttributeValue; import org.alfresco.repo.management.subsystems.ActivateableBean; import org.alfresco.repo.management.subsystems.ChildApplicationContextManager; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.attributes.AttributeService; import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.AuthorityType; import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.util.AbstractLifecycleBean; import org.alfresco.util.PropertyMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; /** * A ChainingUserRegistrySynchronizer is responsible for synchronizing Alfresco's local user (person) and @@ -67,15 +74,17 @@ import org.springframework.context.ApplicationContext; * false then only those users and groups modified since the most recent modification date of all the * objects last queried from the same {@link UserRegistry} are retrieved. In this mode, local users and groups are * created and updated, but not deleted (except where a name collision with a lower priority {@link UserRegistry} is - * detected). This 'differential' mode is much faster, and by default is triggered by + * detected). This 'differential' mode is much faster, and by default is triggered on subsystem startup and also by * {@link #createMissingPerson(String)} when a user is successfully authenticated who doesn't yet have a local person * object in Alfresco. This should mean that new users and their group information are pulled over from LDAP servers as * and when required. * * @author dward */ -public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronizer +public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean implements UserRegistrySynchronizer { + /** The number of users / groups we add at a time in a transaction **/ + private static final int BATCH_SIZE = 10; /** The logger. */ private static final Log logger = LogFactory.getLog(ChainingUserRegistrySynchronizer.class); @@ -104,9 +113,15 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize /** The attribute service. */ private AttributeService attributeService; - /** Should we trigger a sync when missing people log in? */ + /** The retrying transaction helper. */ + private RetryingTransactionHelper retryingTransactionHelper; + + /** Should we trigger a differential sync when missing people log in? */ private boolean syncWhenMissingPeopleLogIn = true; + /** Should we trigger a differential sync on startup? */ + private boolean syncOnStartup = true; + /** Should we auto create a missing person on log in? */ private boolean autoCreatePeopleOnLogin = true; @@ -165,6 +180,17 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize this.attributeService = attributeService; } + /** + * Sets the retrying transaction helper. + * + * @param retryingTransactionHelper + * the new retrying transaction helper + */ + public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) + { + this.retryingTransactionHelper = retryingTransactionHelper; + } + /** * Controls whether we auto create a missing person on log in * @@ -177,7 +203,7 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize } /** - * Controls whether we trigger a sync when missing people log in + * Controls whether we trigger a differential sync when missing people log in * * @param syncWhenMissingPeopleLogIn * if we should trigger a sync when missing people log in @@ -187,11 +213,22 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize this.syncWhenMissingPeopleLogIn = syncWhenMissingPeopleLogIn; } + /** + * Controls whether we trigger a differential sync when the subsystem starts up + * + * @param syncOnStartup + * if we should trigger a sync on startup + */ + public void setSyncOnStartup(boolean syncOnStartup) + { + this.syncOnStartup = syncOnStartup; + } + /* * (non-Javadoc) - * @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#synchronize(boolean) + * @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#synchronize(boolean, boolean) */ - public void synchronize(boolean force) + public void synchronize(boolean force, boolean splitTxns) { Set visitedZoneIds = new TreeSet(); for (String id : this.applicationContextManager.getInstanceIds()) @@ -214,8 +251,14 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize + id + "'; some users and groups previously created by synchronization with this user registry may be removed."); } - int personsProcessed = syncPersonsWithPlugin(id, plugin, force, visitedZoneIds); - int groupsProcessed = syncGroupsWithPlugin(id, plugin, force, visitedZoneIds); + // Work out whether we should do the work in a separate transaction (it's most performant if we + // bunch it into small transactions, but if we are doing a sync on login, it has to be the same + // transaction) + boolean requiresNew = splitTxns + || AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY; + + int personsProcessed = syncPersonsWithPlugin(id, plugin, force, requiresNew, visitedZoneIds); + int groupsProcessed = syncGroupsWithPlugin(id, plugin, force, requiresNew, visitedZoneIds); if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { ChainingUserRegistrySynchronizer.logger @@ -243,7 +286,16 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize { if (this.syncWhenMissingPeopleLogIn) { - synchronize(false); + try + { + synchronize(false, false); + } + catch (Exception e) + { + // We don't want to fail the whole login if we can help it + ChainingUserRegistrySynchronizer.logger.warn( + "User authenticated but failed to sync with user registry", e); + } if (this.personService.personExists(userName)) { return true; @@ -273,21 +325,29 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize * @param force * true if all persons are to be queried. false if only those changed since the * most recent queried user should be queried. + * @param splitTxns + * Can the modifications to Alfresco be split across multiple transactions for maximum performance? If + * true, users and groups are created/updated in batches of 10 for increased performance. If + * false, all users and groups are processed in the current transaction. This is required if + * calling synchronously (e.g. in response to an authentication event in the same transaction). * @param visitedZoneIds * the set of zone ids already processed. These zones have precedence over the current zone when it comes * to user name 'collisions'. If a user is queried that already exists locally but is tagged with one of * the zones in this set, then it will be ignored as this zone has lower priority. * @return the number of users processed */ - private int syncPersonsWithPlugin(String zone, UserRegistry userRegistry, boolean force, Set visitedZoneIds) + private int syncPersonsWithPlugin(String zone, UserRegistry userRegistry, boolean force, boolean splitTxns, + final Set visitedZoneIds) { // Create a prefixed zone ID for use with the authority service - String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone; + final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone; - int processedCount = 0; - long lastModifiedMillis = force ? -1L : getMostRecentUpdateTime( + // The set of zones we associate with new objects (default plus registry specific) + final Set zoneSet = getZones(zoneId); + + final long lastModifiedMillis = force ? -1L : getMostRecentUpdateTime( ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId); - Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis); + final Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis); if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { if (lastModified == null) @@ -300,82 +360,131 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize + DateFormat.getDateTimeInstance().format(lastModified) + " from user registry '" + zone + "'"); } } - Iterator persons = userRegistry.getPersons(lastModified); - Set personsToDelete = this.authorityService.getAllAuthoritiesInZone(zoneId, AuthorityType.USER); + final Iterator persons = userRegistry.getPersons(lastModified); + final Set personsCreated = new TreeSet(); + + class CreationWorker implements RetryingTransactionCallback + { + private long latestTime = lastModifiedMillis; + + public long getLatestTime() + { + return this.latestTime; + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback#execute() + */ + public Integer execute() throws Throwable + { + int processedCount = 0; + do + { + NodeDescription person = persons.next(); + PropertyMap personProperties = person.getProperties(); + String personName = (String) personProperties.get(ContentModel.PROP_USERNAME); + + Set zones = ChainingUserRegistrySynchronizer.this.authorityService + .getAuthorityZones(personName); + if (zones == null) + { + // The person did not exist at all + if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) + { + ChainingUserRegistrySynchronizer.logger.info("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.isInfoEnabled()) + { + ChainingUserRegistrySynchronizer.logger.info("Updating user '" + personName + "'"); + } + ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName, + personProperties); + } + else + { + // The person does not exist in this zone, but may exist in another zone + zones.retainAll(visitedZoneIds); + if (zones.size() > 0) + { + // A person that exists in a different zone with higher precedence + 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 manually or through synchronization with a lower priority user registry."); + } + ChainingUserRegistrySynchronizer.this.personService.deletePerson(personName); + ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet); + } + // Increment the count of processed people + personsCreated.add(personName); + processedCount++; + + // Maintain the last modified date + Date personLastModified = person.getLastModified(); + if (personLastModified != null) + { + this.latestTime = Math.max(this.latestTime, personLastModified.getTime()); + } + } + while (persons.hasNext() && processedCount < ChainingUserRegistrySynchronizer.BATCH_SIZE); + return processedCount; + } + } + + CreationWorker creations = new CreationWorker(); + int processedCount = 0; while (persons.hasNext()) { - NodeDescription person = persons.next(); - PropertyMap personProperties = person.getProperties(); - String personName = (String) personProperties.get(ContentModel.PROP_USERNAME); - if (personsToDelete.remove(personName)) - { - // The person already existed in this zone: update the person - if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) - { - ChainingUserRegistrySynchronizer.logger.info("Updating user '" + personName + "'"); - } - this.personService.setPersonProperties(personName, personProperties); - } - else - { - // The person does not exist in this zone, but may exist in another zone - Set zones = this.authorityService.getAuthorityZones(personName); - if (zones != null) - { - zones.retainAll(visitedZoneIds); - if (zones.size() > 0) - { - // A person that exists in a different zone with higher precedence - 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 manually or through synchronization with a lower priority user registry."); - } - this.personService.deletePerson(personName); - } - else - { - // The person did not exist at all - if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) - { - ChainingUserRegistrySynchronizer.logger.info("Creating user '" + personName + "'"); - } - } - this.personService.createPerson(personProperties, getZones(zoneId)); - } - // Increment the count of processed people - processedCount++; - - // Maintain the last modified date - Date personLastModified = person.getLastModified(); - if (personLastModified != null) - { - lastModifiedMillis = Math.max(lastModifiedMillis, personLastModified.getTime()); - } + processedCount += this.retryingTransactionHelper.doInTransaction(creations, false, splitTxns); + } + long latestTime = creations.getLatestTime(); + if (latestTime != -1) + { + setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId, latestTime); } - if (force && !personsToDelete.isEmpty()) + // Handle deletions if we are doing a full sync + if (force) { - for (String personName : personsToDelete) + class DeletionWorker implements RetryingTransactionCallback { - if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + /* + * (non-Javadoc) + * @see org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback#execute() + */ + public Integer execute() throws Throwable { - ChainingUserRegistrySynchronizer.logger.warn("Deleting user '" + personName + "'"); + int processedCount = 0; + Set personsToDelete = ChainingUserRegistrySynchronizer.this.authorityService + .getAllAuthoritiesInZone(zoneId, AuthorityType.USER); + personsToDelete.removeAll(personsCreated); + for (String personName : personsToDelete) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger.warn("Deleting user '" + personName + "'"); + } + ChainingUserRegistrySynchronizer.this.personService.deletePerson(personName); + processedCount++; + } + return processedCount; } - this.personService.deletePerson(personName); - processedCount++; } - } - if (lastModifiedMillis != -1) - { - setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId, - lastModifiedMillis); + // Just use a single transaction + DeletionWorker deletions = new DeletionWorker(); + processedCount += this.retryingTransactionHelper.doInTransaction(deletions, false, splitTxns); } // Remember we have visited this zone @@ -395,21 +504,30 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize * @param force * true if all groups are to be queried. false if only those changed since the * most recent queried group should be queried. + * @param splitTxns + * Can the modifications to Alfresco be split across multiple transactions for maximum performance? If + * true, users and groups are created/updated in batches of 10 for increased performance. If + * false, all users and groups are processed in the current transaction. This is required if + * calling synchronously (e.g. in response to an authentication event in the same transaction). * @param visitedZoneIds * the set of zone ids already processed. These zones have precedence over the current zone when it comes * to group name 'collisions'. If a group is queried that already exists locally but is tagged with one * of the zones in this set, then it will be ignored as this zone has lower priority. * @return the number of groups processed */ - private int syncGroupsWithPlugin(String zone, UserRegistry userRegistry, boolean force, Set visitedZoneIds) + private int syncGroupsWithPlugin(String zone, UserRegistry userRegistry, boolean force, boolean splitTxns, + final Set visitedZoneIds) { // Create a prefixed zone ID for use with the authority service - String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone; + final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone; - int processedCount = 0; - long lastModifiedMillis = force ? -1L : getMostRecentUpdateTime( + // The set of zones we associate with new objects (default plus registry specific) + final Set zoneSet = getZones(zoneId); + + final long lastModifiedMillis = force ? -1L : getMostRecentUpdateTime( ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId); - Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis); + final Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis); + if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { if (lastModified == null) @@ -423,137 +541,231 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize } } - Iterator groups = userRegistry.getGroups(lastModified); - Map> groupAssocsToCreate = new TreeMap>(); - Set groupsToDelete = this.authorityService.getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP); + final Iterator groups = userRegistry.getGroups(lastModified); + final Map> groupAssocsToCreate = new TreeMap>(); + final Set groupsCreated = new TreeSet(); + + class CreationWorker implements RetryingTransactionCallback + { + private long latestTime = lastModifiedMillis; + + public long getLatestTime() + { + return this.latestTime; + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback#execute() + */ + public Integer execute() throws Throwable + { + int processedCount = 0; + do + { + NodeDescription group = groups.next(); + PropertyMap groupProperties = group.getProperties(); + String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME); + Set 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 + "'"); + } + // create the group + ChainingUserRegistrySynchronizer.this.authorityService.createAuthority(AuthorityType + .getAuthorityType(groupName), groupShortName, (String) groupProperties + .get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME), zoneSet); + Set children = group.getChildAssociations(); + if (!children.isEmpty()) + { + groupAssocsToCreate.put(groupName, children); + } + } + else if (groupZones.contains(zoneId)) + { + // The group already existed in this zone: update the group + Set oldChildren = ChainingUserRegistrySynchronizer.this.authorityService + .getContainedAuthorities(null, groupName, true); + Set newChildren = group.getChildAssociations(); + Set toDelete = new TreeSet(oldChildren); + Set toAdd = new TreeSet(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) + { + // A group that exists in a different zone with higher precedence + continue; + } + String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService + .getShortName(groupName); + // 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 manually or 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 children = group.getChildAssociations(); + if (!children.isEmpty()) + { + groupAssocsToCreate.put(groupName, children); + } + } + + // Increment the count of processed groups + processedCount++; + groupsCreated.add(groupName); + + // Maintain the last modified date + Date groupLastModified = group.getLastModified(); + if (groupLastModified != null) + { + this.latestTime = Math.max(this.latestTime, groupLastModified.getTime()); + } + + } + while (groups.hasNext() && processedCount < ChainingUserRegistrySynchronizer.BATCH_SIZE); + return processedCount; + } + } + + CreationWorker creations = new CreationWorker(); + int processedCount = 0; while (groups.hasNext()) { - NodeDescription group = groups.next(); - PropertyMap groupProperties = group.getProperties(); - String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME); - if (groupsToDelete.remove(groupName)) - { - // update an existing group in the same zone - Set oldChildren = this.authorityService.getContainedAuthorities(null, groupName, true); - Set newChildren = group.getChildAssociations(); - Set toDelete = new TreeSet(oldChildren); - Set toAdd = new TreeSet(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 '" - + this.authorityService.getShortName(child) + "' from group '" - + this.authorityService.getShortName(groupName) + "'"); - } - this.authorityService.removeAuthority(groupName, child); - } - } - else - { - String groupShortName = this.authorityService.getShortName(groupName); - Set groupZones = this.authorityService.getAuthorityZones(groupName); - if (groupZones != null) - { - groupZones.retainAll(visitedZoneIds); - if (groupZones.size() > 0) - { - // 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 manually or through synchronization with a lower priority user registry."); - } - this.authorityService.deleteAuthority(groupName); - } - else - { - if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) - { - ChainingUserRegistrySynchronizer.logger.info("Creating group '" + groupShortName + "'"); - } - } - - // create the group - this.authorityService.createAuthority(AuthorityType.getAuthorityType(groupName), groupShortName, - (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME), getZones(zoneId)); - Set children = group.getChildAssociations(); - if (!children.isEmpty()) - { - groupAssocsToCreate.put(groupName, children); - } - } - - // Increment the count of processed groups - processedCount++; - - // Maintain the last modified date - Date groupLastModified = group.getLastModified(); - if (groupLastModified != null) - { - lastModifiedMillis = Math.max(lastModifiedMillis, groupLastModified.getTime()); - } + processedCount += this.retryingTransactionHelper.doInTransaction(creations, false, splitTxns); + } + long latestTime = creations.getLatestTime(); + if (latestTime != -1) + { + setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, latestTime); } // Add the new associations, now that we have created everything - for (Map.Entry> entry : groupAssocsToCreate.entrySet()) + + final Iterator>> groupAssocs = groupAssocsToCreate.entrySet().iterator(); + class AssocWorker implements RetryingTransactionCallback { - for (String child : entry.getValue()) + /* + * (non-Javadoc) + * @see org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback#execute() + */ + public Integer execute() throws Throwable { - String groupName = entry.getKey(); - if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) + int processedCount = 0; + do { - ChainingUserRegistrySynchronizer.logger.info("Adding '" + this.authorityService.getShortName(child) - + "' to group '" + this.authorityService.getShortName(groupName) + "'"); - } - try - { - this.authorityService.addAuthority(groupName, child); - } - catch (Exception e) - { - // Let's not allow referential integrity problems (dangling references) kill the whole process - if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + + Map.Entry> entry = groupAssocs.next(); + for (String child : entry.getValue()) { - ChainingUserRegistrySynchronizer.logger.warn("Failed to add '" - + this.authorityService.getShortName(child) + "' to group '" - + this.authorityService.getShortName(groupName) + "'", e); + String groupName = entry.getKey(); + if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) + { + ChainingUserRegistrySynchronizer.logger.info("Adding '" + + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child) + + "' to group '" + + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(groupName) + + "'"); + } + try + { + ChainingUserRegistrySynchronizer.this.authorityService.addAuthority(groupName, child); + } + catch (Exception e) + { + // Let's not allow referential integrity problems (dangling references) kill the whole + // process + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger.warn("Failed to add '" + + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child) + + "' to group '" + + ChainingUserRegistrySynchronizer.this.authorityService + .getShortName(groupName) + "'", e); + } + } + } } - } + while (groupAssocs.hasNext() && processedCount < ChainingUserRegistrySynchronizer.BATCH_SIZE); + return processedCount; + } + } + + AssocWorker assocs = new AssocWorker(); + while (groupAssocs.hasNext()) + { + this.retryingTransactionHelper.doInTransaction(assocs, false, splitTxns); } // Delete groups if we have complete information for the zone - if (force && !groupsToDelete.isEmpty()) + if (force) { - for (String group : groupsToDelete) + class DeletionWorker implements RetryingTransactionCallback { - if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + /* + * (non-Javadoc) + * @see org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback#execute() + */ + public Integer execute() throws Throwable { - ChainingUserRegistrySynchronizer.logger.warn("Deleting group '" - + this.authorityService.getShortName(group) + "'"); + int processedCount = 0; + Set groupsToDelete = ChainingUserRegistrySynchronizer.this.authorityService + .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP); + groupsToDelete.removeAll(groupsCreated); + for (String group : groupsToDelete) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger.warn("Deleting group '" + + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(group) + "'"); + } + ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(group); + processedCount++; + } + return processedCount; } - this.authorityService.deleteAuthority(group); - processedCount++; } - } - if (lastModifiedMillis != -1) - { - setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId, - lastModifiedMillis); + // Just use a single transaction + DeletionWorker deletions = new DeletionWorker(); + processedCount += this.retryingTransactionHelper.doInTransaction(deletions, false, splitTxns); } // Remember we have visited this zone @@ -615,9 +827,49 @@ public class ChainingUserRegistrySynchronizer implements UserRegistrySynchronize */ private Set getZones(String zoneId) { - HashSet zones = new HashSet(2, 1.0f); + Set zones = new HashSet(2, 1.0f); zones.add(AuthorityService.ZONE_APP_DEFAULT); zones.add(zoneId); return zones; } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + // Do an initial differential sync on startup, using transaction splitting. This ensures that on the very + // first startup, we don't have to wait for a very long login operation to trigger the first sync! + if (this.syncOnStartup) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + return ChainingUserRegistrySynchronizer.this.retryingTransactionHelper + .doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + try + { + synchronize(false, true); + } + catch (Exception e) + { + ChainingUserRegistrySynchronizer.logger.warn( + "Failed initial synchronize with user registries", e); + } + return null; + } + }); + } + }, AuthenticationUtil.getSystemUserName()); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + } + } diff --git a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java index 0c543b53ec..eff2d9dc8f 100644 --- a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java +++ b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java @@ -26,6 +26,7 @@ package org.alfresco.repo.security.sync; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; @@ -33,18 +34,21 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import junit.framework.TestCase; + import org.alfresco.model.ContentModel; import org.alfresco.repo.management.subsystems.ChildApplicationContextManager; +import org.alfresco.repo.security.authentication.AuthenticationContext; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.AuthorityType; import org.alfresco.service.cmr.security.PersonService; -import org.alfresco.util.BaseSpringTest; import org.alfresco.util.PropertyMap; -import org.hibernate.SessionFactory; -import org.hibernate.engine.SessionFactoryImplementor; import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.StaticApplicationContext; /** @@ -52,7 +56,7 @@ import org.springframework.context.support.StaticApplicationContext; * * @author dward */ -public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest +public class ChainingUserRegistrySynchronizerTest extends TestCase { /** The context locations, in reverse priority order. */ @@ -61,6 +65,10 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest "classpath:alfresco/application-context.xml", "classpath:sync-test-context.xml" }; + /** The Spring application context */ + private static ApplicationContext context = new ClassPathXmlApplicationContext( + ChainingUserRegistrySynchronizerTest.CONFIG_LOCATIONS); + /** The synchronizer we are testing. */ private UserRegistrySynchronizer synchronizer; @@ -76,47 +84,36 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest /** The node service. */ private NodeService nodeService; - /* - * (non-Javadoc) - * @see org.springframework.test.AbstractTransactionalSpringContextTests#onSetUpInTransaction() - */ + /** The authentication context. */ + private AuthenticationContext authenticationContext; + + /** The retrying transaction helper. */ + private RetryingTransactionHelper retryingTransactionHelper; + @Override - protected void onSetUpInTransaction() throws Exception + protected void setUp() throws Exception { - ApplicationContext context = getApplicationContext(); - this.synchronizer = (UserRegistrySynchronizer) context.getBean("testUserRegistrySynchronizer"); - this.applicationContextManager = (MockApplicationContextManager) context + this.synchronizer = (UserRegistrySynchronizer) ChainingUserRegistrySynchronizerTest.context + .getBean("testUserRegistrySynchronizer"); + this.applicationContextManager = (MockApplicationContextManager) ChainingUserRegistrySynchronizerTest.context .getBean("testApplicationContextManager"); - this.personService = (PersonService) context.getBean("personService"); - this.authorityService = (AuthorityService) context.getBean("authorityService"); - this.nodeService = (NodeService) context.getBean("nodeService"); + this.personService = (PersonService) ChainingUserRegistrySynchronizerTest.context.getBean("personService"); + this.authorityService = (AuthorityService) ChainingUserRegistrySynchronizerTest.context + .getBean("authorityService"); + this.nodeService = (NodeService) ChainingUserRegistrySynchronizerTest.context.getBean("nodeService"); + + this.authenticationContext = (AuthenticationContext) ChainingUserRegistrySynchronizerTest.context + .getBean("authenticationContext"); + this.authenticationContext.setSystemUserAsCurrentUser(); + + this.retryingTransactionHelper = (RetryingTransactionHelper) ChainingUserRegistrySynchronizerTest.context + .getBean("retryingTransactionHelper"); } - /* - * (non-Javadoc) - * @see org.springframework.test.AbstractTransactionalSpringContextTests#onTearDownInTransaction() - */ - protected void onTearDownInTransaction() throws Exception - { - flushAndClear(); - - // Try to clear the Hibernate L2 cache so we have consistency after a rollback - SessionFactory sessionFactory = getSession().getSessionFactory(); - String[] persistentClasses = ((SessionFactoryImplementor) sessionFactory).getImplementors("java.lang.Object"); - for (String persistentClass : persistentClasses) - { - sessionFactory.evictEntity(persistentClass); - } - } - - /* - * (non-Javadoc) - * @see org.alfresco.util.BaseSpringTest#getConfigLocations() - */ @Override - protected String[] getConfigLocations() + protected void tearDown() throws Exception { - return ChainingUserRegistrySynchronizerTest.CONFIG_LOCATIONS; + this.authenticationContext.clearCurrentSecurityContext(); } /** @@ -136,7 +133,7 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest * @throws Exception * the exception */ - public void setUpTestUsersAndGroups() throws Exception + private void setUpTestUsersAndGroups() throws Exception { this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[] { @@ -152,19 +149,73 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest { newGroup("G2", "U1", "U3", "U4"), newGroup("G6", "U3", "U4", "G7"), newGroup("G7", "U5") })); - this.synchronizer.synchronize(true); - assertExists("Z1", "U1"); - assertExists("Z1", "U2"); - assertExists("Z1", "G1"); - assertExists("Z1", "G2", "U1", "G3"); - assertExists("Z1", "G3", "U2", "G4", "G5"); - assertExists("Z1", "G4"); - assertExists("Z1", "G5"); - assertExists("Z2", "U3"); - assertExists("Z2", "U4"); - assertExists("Z2", "U5"); - assertExists("Z2", "G6", "U3", "U4", "G7"); - assertExists("Z2", "G7", "U5"); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + ChainingUserRegistrySynchronizerTest.this.synchronizer.synchronize(true, true); + return null; + } + }); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + assertExists("Z1", "U1"); + assertExists("Z1", "U2"); + assertExists("Z1", "G1"); + assertExists("Z1", "G2", "U1", "G3"); + assertExists("Z1", "G3", "U2", "G4", "G5"); + assertExists("Z1", "G4"); + assertExists("Z1", "G5"); + assertExists("Z2", "U3"); + assertExists("Z2", "U4"); + assertExists("Z2", "U5"); + assertExists("Z2", "G6", "U3", "U4", "G7"); + assertExists("Z2", "G7", "U5"); + return null; + } + }); + } + + private void tearDownTestUsersAndGroups() throws Exception + { + // Wipe out everything that was in Z1 and Z2 + this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z1", new NodeDescription[] {}, + new NodeDescription[] {}), new MockUserRegistry("Z2", new NodeDescription[] {}, + new NodeDescription[] {})); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + ChainingUserRegistrySynchronizerTest.this.synchronizer.synchronize(true, true); + return null; + } + }); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + assertNotExists("U1"); + assertNotExists("U2"); + assertNotExists("U3"); + assertNotExists("U4"); + assertNotExists("U5"); + assertNotExists("U6"); + assertNotExists("G1"); + assertNotExists("G2"); + assertNotExists("G3"); + assertNotExists("G4"); + assertNotExists("G5"); + assertNotExists("G6"); + assertNotExists("G7"); + return null; + } + }); } /** @@ -200,22 +251,33 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest { newGroup("G2", "U1", "U3", "U4", "U6"), newGroup("G7") })); - this.synchronizer.synchronize(false); - assertExists("Z1", "U1"); - assertEmailEquals("U1", "changeofemail@alfresco.com"); - assertExists("Z1", "U2"); - assertExists("Z1", "U6"); - assertExists("Z1", "G1", "U1", "U6"); - assertExists("Z1", "G2", "U1"); - assertExists("Z1", "G3", "U2", "G4", "G5"); - assertExists("Z1", "G4"); - assertExists("Z1", "G5", "U6"); - assertExists("Z2", "U3"); - assertExists("Z2", "U4"); - assertExists("Z2", "U5"); - assertEmailEquals("U5", "u5email@alfresco.com"); - assertExists("Z2", "G6", "U3", "U4", "G7"); - assertExists("Z2", "G7"); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + + ChainingUserRegistrySynchronizerTest.this.synchronizer.synchronize(false, false); + // Stay in the same transaction + assertExists("Z1", "U1"); + assertEmailEquals("U1", "changeofemail@alfresco.com"); + assertExists("Z1", "U2"); + assertExists("Z1", "U6"); + assertExists("Z1", "G1", "U1", "U6"); + assertExists("Z1", "G2", "U1"); + assertExists("Z1", "G3", "U2", "G4", "G5"); + assertExists("Z1", "G4"); + assertExists("Z1", "G5", "U6"); + assertExists("Z2", "U3"); + assertExists("Z2", "U4"); + assertExists("Z2", "U5"); + assertEmailEquals("U5", "u5email@alfresco.com"); + assertExists("Z2", "G6", "U3", "U4", "G7"); + assertExists("Z2", "G7"); + return null; + } + }); + tearDownTestUsersAndGroups(); } /** @@ -255,21 +317,38 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest { newGroup("G2", "U1", "U3", "U4", "U6"), newGroup("G6", "U3", "U4", "G7"), newGroup("G7", "U4", "U5") })); - this.synchronizer.synchronize(true); - assertExists("Z1", "U2"); - assertExists("Z1", "U3"); - assertExists("Z1", "U6"); - assertExists("Z1", "G1", "U6"); - assertExists("Z1", "G2"); - assertExists("Z1", "G3", "U2", "G5"); - assertNotExists("G4"); - assertExists("Z1", "G5", "U6"); - assertExists("Z1", "G6", "U3"); - assertExists("Z2", "U1"); - assertEmailEquals("U1", "somenewemail@alfresco.com"); - assertNotExists("U4"); - assertNotExists("U5"); - assertExists("Z2", "G7"); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + ChainingUserRegistrySynchronizerTest.this.synchronizer.synchronize(true, true); + return null; + } + }); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + assertExists("Z1", "U2"); + assertExists("Z1", "U3"); + assertExists("Z1", "U6"); + assertExists("Z1", "G1", "U6"); + assertExists("Z1", "G2"); + assertExists("Z1", "G3", "U2", "G5"); + assertNotExists("G4"); + assertExists("Z1", "G5", "U6"); + assertExists("Z1", "G6", "U3"); + assertExists("Z2", "U1"); + assertEmailEquals("U1", "somenewemail@alfresco.com"); + assertNotExists("U4"); + assertNotExists("U5"); + assertExists("Z2", "G7"); + return null; + } + }); + tearDownTestUsersAndGroups(); } /** @@ -348,7 +427,8 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest assertTrue(this.authorityService.authorityExists(longName)); // Check in correct zone - assertTrue(this.authorityService.getAuthorityZones(longName).contains(AuthorityService.ZONE_AUTH_EXT_PREFIX+zone)); + assertTrue(this.authorityService.getAuthorityZones(longName).contains( + AuthorityService.ZONE_AUTH_EXT_PREFIX + zone)); if (AuthorityType.getAuthorityType(longName).equals(AuthorityType.GROUP)) { // Check groups have expected members @@ -483,7 +563,7 @@ public class ChainingUserRegistrySynchronizerTest extends BaseSpringTest { /** The contexts. */ - private Map contexts; + private Map contexts = Collections.emptyMap(); /** * Sets the user registries. diff --git a/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java b/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java index 30ab728990..90b9a11c2a 100644 --- a/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java +++ b/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java @@ -31,7 +31,7 @@ package org.alfresco.repo.security.sync; * @author dward */ public interface UserRegistrySynchronizer -{ +{ /** * Creates a person object for a successfully authenticated user who does not yet have a person object, if allowed * to by configuration. Depending on configuration, may trigger a partial synchronize and/or create a new person @@ -42,7 +42,7 @@ public interface UserRegistrySynchronizer * @return true, if a person is created */ public boolean createMissingPerson(String username); - + /** * Retrieves timestamped user and group information from configured external sources and compares it with the local * users and groups last retrieved from the same sources. Any updates and additions made to those users and groups @@ -56,6 +56,11 @@ public interface UserRegistrySynchronizer * queried for those users and groups modified since the most recent modification date of all the objects * last queried from that same source. In this mode, local users and groups are created and updated, but * not deleted. + * @param splitTxns + * Can the modifications to Alfresco be split across multiple transactions for maximum performance? If + * true, users and groups are created/updated in batches of 10 for increased performance. If + * false, 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). */ - public void synchronize(boolean force); + public void synchronize(boolean force, boolean splitTxns); } \ No newline at end of file diff --git a/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizerJob.java b/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizerJob.java index 60ad687133..0a48fb9233 100644 --- a/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizerJob.java +++ b/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizerJob.java @@ -55,7 +55,7 @@ public class UserRegistrySynchronizerJob implements Job public Object doWork() throws Exception { userRegistrySynchronizer.synchronize(synchronizeChangesOnly == null - || !Boolean.parseBoolean(synchronizeChangesOnly)); + || !Boolean.parseBoolean(synchronizeChangesOnly), true); return null; } }, AuthenticationUtil.getSystemUserName()); diff --git a/source/test-resources/sync-test-context.xml b/source/test-resources/sync-test-context.xml index 9e0e967ec8..8eef1087c8 100644 --- a/source/test-resources/sync-test-context.xml +++ b/source/test-resources/sync-test-context.xml @@ -15,6 +15,9 @@ + + + userRegistry