diff --git a/config/alfresco/dao/dao-context.xml b/config/alfresco/dao/dao-context.xml index 74e81924ab..e48997b2d8 100644 --- a/config/alfresco/dao/dao-context.xml +++ b/config/alfresco/dao/dao-context.xml @@ -264,6 +264,7 @@ + diff --git a/config/alfresco/model/systemModel.xml b/config/alfresco/model/systemModel.xml index 96b9241172..de54824175 100644 --- a/config/alfresco/model/systemModel.xml +++ b/config/alfresco/model/systemModel.xml @@ -396,6 +396,26 @@ + + + Pending fix ACL + + + Shared Acl To Replace + d:long + true + false + + + + Inherit From + d:long + true + false + + + + \ No newline at end of file diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 8a29bd07eb..ec0185059a 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -102,8 +102,19 @@ + + + + + + + + + + + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 4a10a1f4e0..ef8b3e5111 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -1110,6 +1110,20 @@ models.enforceTenantInNamespace=false # Allowed protocols for links links.protocosl.white.list=http,https,ftp,mailto +# Fixed ACLs +# Required for fixing MNT-15368 - Time Consumed for Updating Folder Permission +# ADMAccessControlListDAO.setFixedAcls called on a large folder hierarchy will take a long time for its execution. +# For this reason now method can also be called asynchronously if transaction reaches system.fixedACLs.maxTransactionTime. +# In this case setFixedAcls method recursion will be stopped and unfinished nodes will be marked with ASPECT_PENDING_FIX_ACL. +# Pending nodes will be processed by FixedAclUpdater, programmatically called but also configured as a scheduled job. +system.fixedACLs.maxTransactionTime=10000 +# fixedACLsUpdater - lock time to live +system.fixedACLsUpdater.lockTTL=10000 +# fixedACLsUpdater - maximum number of nodes to process per execution +system.fixedACLsUpdater.maxItemBatchSize=100 +# fixedACLsUpdater cron expression - fire at midnight every day +system.fixedACLsUpdater.cronExpression=0 0 0 * * ? + cmis.disable.hidden.leading.period.files=false #Smart Folders Config Properties diff --git a/config/alfresco/scheduled-jobs-context.xml b/config/alfresco/scheduled-jobs-context.xml index 65a92d0587..4445bea1de 100644 --- a/config/alfresco/scheduled-jobs-context.xml +++ b/config/alfresco/scheduled-jobs-context.xml @@ -327,4 +327,26 @@ ${system.cronJob.startDelayMinutes} + + + + + + org.alfresco.repo.domain.permissions.FixedAclUpdaterJob + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java b/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java index 7dc7bf4de3..9b95f7f2f2 100644 --- a/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java +++ b/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java @@ -18,10 +18,13 @@ */ package org.alfresco.repo.domain.permissions; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.alfresco.model.ContentModel; import org.alfresco.repo.domain.node.NodeDAO; @@ -32,11 +35,15 @@ import org.alfresco.repo.security.permissions.AccessControlList; import org.alfresco.repo.security.permissions.AccessControlListProperties; import org.alfresco.repo.security.permissions.SimpleAccessControlListProperties; import org.alfresco.repo.security.permissions.impl.AclChange; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.dao.ConcurrencyFailureException; /** @@ -47,6 +54,7 @@ import org.springframework.dao.ConcurrencyFailureException; */ public class ADMAccessControlListDAO implements AccessControlListDAO { + private static final Log log = LogFactory.getLog(ADMAccessControlListDAO.class); /** * The DAO for Nodes. */ @@ -56,6 +64,9 @@ public class ADMAccessControlListDAO implements AccessControlListDAO private BehaviourFilter behaviourFilter; private boolean preserveAuditableData = true; + + /**maxim transaction time allowed for {@link #setFixedAcls(Long, Long, Long, Long, List, boolean, AsyncCallParameters, boolean)} */ + private long fixedAclMaxTransactionTime = 10 * 1000; public void setNodeDAO(NodeDAO nodeDAO) { @@ -67,6 +78,11 @@ public class ADMAccessControlListDAO implements AccessControlListDAO this.aclDaoComponent = aclDaoComponent; } + public void setFixedAclMaxTransactionTime(long fixedAclMaxTransactionTime) + { + this.fixedAclMaxTransactionTime = fixedAclMaxTransactionTime; + } + public void setBehaviourFilter(BehaviourFilter behaviourFilter) { this.behaviourFilter = behaviourFilter; @@ -295,16 +311,23 @@ public class ADMAccessControlListDAO implements AccessControlListDAO } setAccessControlList(nodeRef, aclId); } - + public void setAccessControlList(StoreRef storeRef, Acl acl) { throw new UnsupportedOperationException(); } - + public List setInheritanceForChildren(NodeRef parent, Long inheritFrom, Long sharedAclToReplace) + { + //check transaction resource to determine if async call may be required + boolean asyncCall = AlfrescoTransactionSupport.getResource(FixedAclUpdater.FIXED_ACL_ASYNC_CALL_KEY) == null ? false : true; + return setInheritanceForChildren(parent, inheritFrom, sharedAclToReplace, asyncCall); + } + + public List setInheritanceForChildren(NodeRef parent, Long inheritFrom, Long sharedAclToReplace, boolean asyncCall) { List changes = new ArrayList(); - setFixedAcls(getNodeIdNotNull(parent), inheritFrom, null, sharedAclToReplace, changes, false); + setFixedAcls(getNodeIdNotNull(parent), inheritFrom, null, sharedAclToReplace, changes, false, asyncCall, true); return changes; } @@ -329,29 +352,51 @@ public class ADMAccessControlListDAO implements AccessControlListDAO */ public void setFixedAcls(Long nodeId, Long inheritFrom, Long mergeFrom, Long sharedAclToReplace, List changes, boolean set) { + setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, false, true); + } + + /** + * Support to set a shared ACL on a node and all of its children + * + * @param nodeId + * 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 ? + * @param asyncCall + * function may require asynchronous call depending the execution time; if time exceeds configured fixedAclMaxTransactionTime value, + * recursion is stopped using propagateOnChildren parameter(set on false) and those nodes for which the method execution was not finished + * in the classical way, will have ASPECT_PENDING_FIX_ACL, which will be used in {@link FixedAclUpdater} for later processing + */ + public void setFixedAcls(Long nodeId, Long inheritFrom, Long mergeFrom, Long sharedAclToReplace, List changes, boolean set, boolean asyncCall, boolean propagateOnChildren) + { + if (log.isDebugEnabled()) + { + log.debug(" Set fixed acl for nodeId=" + nodeId + " inheritFrom=" + inheritFrom + " sharedAclToReplace=" + sharedAclToReplace + + " mergefrom= " + mergeFrom); + } + if (nodeId == null) { return; } else { - if (set) - { - // Lazily retrieve/create the shared ACL - if (mergeFrom == null) - { - mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom); - } - nodeDAO.setNodeAclId(nodeId, mergeFrom); - } - - // update all shared in one shot - recurse later - + // Lazily retrieve/create the shared ACL if (mergeFrom == null) { mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom); } + if (set) + { + nodeDAO.setNodeAclId(nodeId, mergeFrom); + } List children = nodeDAO.getPrimaryChildrenAcls(nodeId); @@ -359,20 +404,18 @@ public class ADMAccessControlListDAO implements AccessControlListDAO { nodeDAO.setPrimaryChildrenSharedAclId(nodeId, sharedAclToReplace, mergeFrom); } - + + if (!propagateOnChildren) + { + return; + } for (NodeIdAndAclId child : children) { - // Lazily retrieve/create the shared ACL - if (mergeFrom == null) - { - mergeFrom = aclDaoComponent.getInheritedAccessControlList(inheritFrom); - } - Long acl = child.getAclId(); - + if (acl == null) { - setFixedAcls(child.getId(), inheritFrom, mergeFrom, sharedAclToReplace, changes, false); + propagateOnChildren = setFixAclPending(child.getId(), inheritFrom, mergeFrom, sharedAclToReplace, changes, false, asyncCall, propagateOnChildren); } else { @@ -383,7 +426,7 @@ public class ADMAccessControlListDAO implements AccessControlListDAO // Already replaced if(acl.equals(sharedAclToReplace)) { - setFixedAcls(child.getId(), inheritFrom, mergeFrom, sharedAclToReplace, changes, false); + propagateOnChildren = setFixAclPending(child.getId(), inheritFrom, mergeFrom, sharedAclToReplace, changes, false, asyncCall, propagateOnChildren); } else { @@ -409,7 +452,67 @@ public class ADMAccessControlListDAO implements AccessControlListDAO } } } + + /** + * If async call required adds ASPECT_PENDING_FIX_ACL aspect to nodes when transactionTime reaches max admitted time + */ + private boolean setFixAclPending(Long nodeId, Long inheritFrom, Long mergeFrom, Long sharedAclToReplace, List changes, + boolean set, boolean asyncCall, boolean propagateOnChildren) + { + // check if async call is required + if (!asyncCall) + { + // make regular method call + setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, asyncCall, propagateOnChildren); + return true; + } + else + { + // check transaction time + long transactionStartTime = AlfrescoTransactionSupport.getTransactionStartTime(); + long transactionTime = System.currentTimeMillis() - transactionStartTime; + if (transactionTime < fixedAclMaxTransactionTime) + { + // make regular method call if time is under max transaction configured time + setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, asyncCall, propagateOnChildren); + return true; + } + else + { + // time exceeded; + if (nodeDAO.getPrimaryChildrenAcls(nodeId).size() == 0) + { + // if node is leaf in tree hierarchy call setFixedAcls now as processing with FixedAclUpdater would be more time consuming + setFixedAcls(nodeId, inheritFrom, mergeFrom, sharedAclToReplace, changes, set, asyncCall, false); + } + else + { + // set ASPECT_PENDING_FIX_ACL aspect on node to be later on processed with FixedAclUpdater + addFixedAclPendingAspect(nodeId, sharedAclToReplace, inheritFrom); + AlfrescoTransactionSupport.bindResource(FixedAclUpdater.FIXED_ACL_ASYNC_REQUIRED_KEY, true); + } + // stop propagating on children nodes + return false; + } + } + } + + private void addFixedAclPendingAspect(Long nodeId, Long sharedAclToReplace, Long inheritFrom) + { + Set aspect = new HashSet<>(); + aspect.add(ContentModel.ASPECT_PENDING_FIX_ACL); + nodeDAO.addNodeAspects(nodeId, aspect); + Map pendingAclProperties = new HashMap<>(); + pendingAclProperties.put(ContentModel.PROP_SHARED_ACL_TO_REPLACE, sharedAclToReplace); + pendingAclProperties.put(ContentModel.PROP_INHERIT_FROM_ACL, inheritFrom); + nodeDAO.addNodeProperties(nodeId, pendingAclProperties); + if (log.isDebugEnabled()) + { + log.debug("Set Fixed Acl Pending : " + nodeId + " " + nodeDAO.getNodePair(nodeId).getSecond()); + } + } + /** * {@inheritDoc} */ diff --git a/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java b/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java index 21b90ad2cc..faa809ce69 100644 --- a/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java +++ b/source/java/org/alfresco/repo/domain/permissions/AccessControlListDAO.java @@ -76,6 +76,12 @@ public interface AccessControlListDAO * Update inheritance */ public List setInheritanceForChildren(NodeRef parent, Long inheritFrom, Long sharedAclToReplace); + + /** + * Set the inheritance on a given node and it's children. If the operation takes + * too long and asyncCall parameter set accordingly, fixed ACLs method will be synchronously called. + */ + public List setInheritanceForChildren(NodeRef parent, Long inheritFrom, Long sharedAclToReplace, boolean asyncCall); public Long getIndirectAcl(NodeRef nodeRef); diff --git a/source/java/org/alfresco/repo/domain/permissions/FixedAclUpdater.java b/source/java/org/alfresco/repo/domain/permissions/FixedAclUpdater.java new file mode 100644 index 0000000000..57570df828 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/permissions/FixedAclUpdater.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.domain.permissions; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.domain.node.NodeDAO.NodeRefQueryCallback; +import org.alfresco.repo.lock.JobLockService; +import org.alfresco.repo.lock.LockAcquisitionException; +import org.alfresco.repo.lock.JobLockService.JobLockRefreshCallback; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.transaction.TransactionListenerAdapter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.alfresco.util.VmShutdownListener.VmShutdownException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Finds nodes with ASPECT_PENDING_FIX_ACL aspect and sets fixed ACLs for them + * + * @author Andreea Dragoi + * @since 4.2.7 + */ +public class FixedAclUpdater extends TransactionListenerAdapter +{ + private static final Log log = LogFactory.getLog(FixedAclUpdater.class); + private static final Set PENDING_FIX_ACL_ASPECT_PROPS = pendingFixAclAspectProps(); + + public static final String FIXED_ACL_ASYNC_REQUIRED_KEY = "FIXED_ACL_ASYNC_REQUIRED"; + public static final String FIXED_ACL_ASYNC_CALL_KEY = "FIXED_ACL_ASYNC_CALL"; + + private JobLockService jobLockService; + private TransactionService transactionService; + private AccessControlListDAO accessControlListDAO; + private NodeDAO nodeDAO; + private QName lockQName = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "FixedAclUpdater"); + private long lockTimeToLive = 10000; + private long lockRefreshTime = lockTimeToLive / 2; + private int maxItemBatchSize = 100; + + public void setJobLockService(JobLockService jobLockService) + { + this.jobLockService = jobLockService; + } + + public void setNodeDAO(NodeDAO nodeDAO) + { + this.nodeDAO = nodeDAO; + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setAccessControlListDAO(AccessControlListDAO accessControlListDAO) + { + this.accessControlListDAO = accessControlListDAO; + } + + public void setMaxItemBatchSize(int maxItemBatchSize) + { + this.maxItemBatchSize = maxItemBatchSize; + } + + public void setLockTimeToLive(long lockTimeToLive) + { + this.lockTimeToLive = lockTimeToLive; + this.lockRefreshTime = lockTimeToLive / 2; + } + + private static Set pendingFixAclAspectProps() + { + Set props = new HashSet<>(); + props.add(ContentModel.PROP_SHARED_ACL_TO_REPLACE); + props.add(ContentModel.PROP_INHERIT_FROM_ACL); + return props; + } + + private int findAndUpdateAcl(FixedAclUpdaterJobLockRefreshCallback jobCallback) + { + final Set aspects = new HashSet<>(1); + aspects.add(ContentModel.ASPECT_PENDING_FIX_ACL); + + List nodesToUpdate = getNodesWithAspects(aspects); + int processedNodes = 0; + + // loop over results + for (final NodeRef nodeRef : nodesToUpdate) + { + // Check if we have been terminated + if (!jobCallback.isActive.get()) + { + if (log.isDebugEnabled()) + { + log.debug(String.format("Processing node failed %s. Job not active", nodeRef)); + } + // terminate + break; + } + if (log.isDebugEnabled()) + { + log.debug(String.format("Processing node %s", nodeRef)); + } + final Long nodeId = nodeDAO.getNodePair(nodeRef).getFirst(); + + try + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // retrieve acl properties from node + Long inheritFrom = (Long) nodeDAO.getNodeProperty(nodeId, ContentModel.PROP_INHERIT_FROM_ACL); + Long sharedAclToReplace = (Long) nodeDAO.getNodeProperty(nodeId, ContentModel.PROP_SHARED_ACL_TO_REPLACE); + + // set inheritance using retrieved prop + accessControlListDAO.setInheritanceForChildren(nodeRef, inheritFrom, sharedAclToReplace, true); + + nodeDAO.removeNodeAspects(nodeId, aspects); + nodeDAO.removeNodeProperties(nodeId, PENDING_FIX_ACL_ASPECT_PROPS); + + if (log.isDebugEnabled()) + { + log.debug(String.format("Node processed ", nodeRef)); + } + return null; + } + }, false, true); + } + catch (Exception e) + { + if (log.isDebugEnabled()) + { + log.debug(String.format("Could not process node ", nodeRef), e); + } + } + + processedNodes++; + } + + if (log.isDebugEnabled()) + { + log.debug(String.format("Nodes found %s; nodes processed %s", nodesToUpdate.size(), processedNodes)); + } + + return processedNodes; + } + + public void execute() + { + String lockToken = null; + FixedAclUpdaterJobLockRefreshCallback callback = new FixedAclUpdaterJobLockRefreshCallback(); + try + { + RunAsWork findAndUpdateAclRunAsWork = findAndUpdateAclRunAsWork(callback); + lockToken = jobLockService.getLock(lockQName, lockTimeToLive, 0, 1); + while (true) + { + jobLockService.refreshLock(lockToken, lockQName, lockRefreshTime, callback); + Integer processed = AuthenticationUtil.runAs(findAndUpdateAclRunAsWork, AuthenticationUtil.getSystemUserName()); + if (processed.intValue() == 0) + { + // There is no more to process + break; + } + // There is still more to process, so continue + } + } + catch (LockAcquisitionException e) + { + // already running + } + catch (VmShutdownException e) + { + if (log.isDebugEnabled()) + { + log.debug("FixedAclUpdater aborted"); + } + } + finally + { + callback.isActive.set(false); + jobLockService.releaseLock(lockToken, lockQName); + } + } + + private RunAsWork findAndUpdateAclRunAsWork(final FixedAclUpdaterJobLockRefreshCallback callback) + { + final RetryingTransactionCallback findAndUpdateAclWork = new RetryingTransactionCallback() + { + public Integer execute() throws Exception + { + return findAndUpdateAcl(callback); + } + }; + + // execute as system user to ensure fast, accurate results + RunAsWork findAndUpdateAclRunAsWork = new RunAsWork() + { + @Override + public Integer doWork() throws Exception + { + return transactionService.getRetryingTransactionHelper().doInTransaction(findAndUpdateAclWork, false, true); + } + }; + return findAndUpdateAclRunAsWork; + } + + private List getNodesWithAspects(final Set aspects) + { + return transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback>() + { + @Override + public List execute() throws Throwable + { + GetNodesWithAspectCallback callback = new GetNodesWithAspectCallback(); + nodeDAO.getNodesWithAspects(aspects, 1L, null, callback); + return callback.getNodes(); + } + }, false, true); + } + + private class GetNodesWithAspectCallback implements NodeRefQueryCallback + { + private List nodes = new ArrayList<>(); + + @Override + public boolean handle(Pair nodePair) + { + if (nodes.size() < maxItemBatchSize) + { + nodes.add(nodePair.getSecond()); + return true; + } + return false; + } + + public List getNodes() + { + return nodes; + } + } + + private static class FixedAclUpdaterJobLockRefreshCallback implements JobLockRefreshCallback + { + public AtomicBoolean isActive = new AtomicBoolean(true); + + @Override + public boolean isActive() + { + return isActive.get(); + } + + @Override + public void lockReleased() + { + isActive.set(false); + } + } + + @Override + public void afterCommit() + { + Thread t = new Thread(new Runnable() + { + @Override + public void run() + { + execute(); + } + }); + t.start(); + } +} diff --git a/source/java/org/alfresco/repo/domain/permissions/FixedAclUpdaterJob.java b/source/java/org/alfresco/repo/domain/permissions/FixedAclUpdaterJob.java new file mode 100644 index 0000000000..f9b39272d7 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/permissions/FixedAclUpdaterJob.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.domain.permissions; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Triggers setFixedAcl for those nodes with ASPECT_PENDING_FIX_ACL + * + * @author Andreea Dragoi + * @since 4.2.7 + * + */ +public class FixedAclUpdaterJob implements Job +{ + + /** + * Calls {@link FixedAclUpdater} to do it's work + */ + @Override + public void execute(JobExecutionContext context) throws JobExecutionException + { + JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); + Object fixedAclUpdaterObject = jobDataMap.get("fixedAclUpdater"); + if (fixedAclUpdaterObject == null || !(fixedAclUpdaterObject instanceof FixedAclUpdater)) + { + throw new AlfrescoRuntimeException("FixedAclUpdaterJob must contain a valid 'fixedAclUpdater'"); + } + FixedAclUpdater fixedAclUpdater = (FixedAclUpdater)fixedAclUpdaterObject; + fixedAclUpdater.execute(); + } +} diff --git a/source/java/org/alfresco/repo/jscript/ScriptNode.java b/source/java/org/alfresco/repo/jscript/ScriptNode.java index fbbbe9969e..dab44b4624 100644 --- a/source/java/org/alfresco/repo/jscript/ScriptNode.java +++ b/source/java/org/alfresco/repo/jscript/ScriptNode.java @@ -1628,6 +1628,18 @@ public class ScriptNode implements Scopeable, NamespacePrefixResolverProvider this.services.getPermissionService().setInheritParentPermissions(this.nodeRef, inherit); } + /** + * Set whether this node should inherit permissions from the parent node. If the operation takes + * too long and asyncCall parameter set accordingly, fixed ACLs method will be asynchronously called. + * + * @param inherit True to inherit parent permissions, false otherwise. + * @param asyncCall True if fixed ACLs should be asynchronously set when operation execution takes too long, false otherwise. + */ + public void setInheritsPermissions(boolean inherit, boolean asyncCall) + { + this.services.getPermissionService().setInheritParentPermissions(this.nodeRef, inherit, asyncCall); + } + /** * Apply a permission for ALL users to the node. * diff --git a/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java index 8a7a8d6749..2967c627a3 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/PermissionServiceImpl.java @@ -34,6 +34,7 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.domain.permissions.AclDAO; import org.alfresco.repo.node.db.traitextender.NodeServiceTrait; +import org.alfresco.repo.domain.permissions.FixedAclUpdater; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; @@ -148,6 +149,8 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm protected AclDAO aclDaoComponent; protected PermissionReference allPermissionReference; + + protected FixedAclUpdater fixedAclUpdater; protected boolean anyDenyDenies = false; @@ -271,7 +274,12 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm { this.aclDaoComponent = aclDaoComponent; } - + + public void setFixedAclUpdater(FixedAclUpdater fixedAclUpdater) + { + this.fixedAclUpdater = fixedAclUpdater; + } + /** * Set the permissions access cache. * @@ -1034,6 +1042,31 @@ public class PermissionServiceImpl extends AbstractLifecycleBean implements Perm permissionsDaoComponent.setInheritParentPermissions(actualRef, inheritParentPermissions); accessCache.clear(); } + + public void setInheritParentPermissions(NodeRef nodeRef, final boolean inheritParentPermissions, boolean asyncCall) + { + final NodeRef actualRef = tenantService.getName(nodeRef); + if (asyncCall) + { + //use transaction resource to determine later on in ADMAccessControlListDAO.setFixedAcl if asynchronous call may be required + AlfrescoTransactionSupport.bindResource(FixedAclUpdater.FIXED_ACL_ASYNC_CALL_KEY, true); + permissionsDaoComponent.setInheritParentPermissions(actualRef, inheritParentPermissions); + //check if asynchronous call was required + Boolean asyncCallRequired = (Boolean) AlfrescoTransactionSupport.getResource(FixedAclUpdater.FIXED_ACL_ASYNC_REQUIRED_KEY); + if (asyncCallRequired != null && asyncCallRequired) + { + //after transaction is committed FixedAclUpdater will be started in a new thread to process pending nodes + AlfrescoTransactionSupport.bindListener(fixedAclUpdater); + } + } + else + { + //regular method call + permissionsDaoComponent.setInheritParentPermissions(actualRef, inheritParentPermissions); + } + + accessCache.clear(); + } /** * @see org.alfresco.service.cmr.security.PermissionService#getInheritParentPermissions(org.alfresco.service.cmr.repository.NodeRef) diff --git a/source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java b/source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java index 6a80378e59..a9bcfa85c7 100644 --- a/source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java +++ b/source/java/org/alfresco/repo/security/permissions/noop/PermissionServiceNOOPImpl.java @@ -114,6 +114,12 @@ public class PermissionServiceNOOPImpl implements PermissionServiceSPI // Do Nothing. } + @Override + public void setInheritParentPermissions(NodeRef nodeRef, boolean inheritParentPermissions, boolean asyncCall) + { + // Do Nothing. + } + @Override public boolean getInheritParentPermissions(NodeRef nodeRef) { diff --git a/source/test-java/org/alfresco/repo/domain/permissions/FixedAclUpdaterTest.java b/source/test-java/org/alfresco/repo/domain/permissions/FixedAclUpdaterTest.java new file mode 100644 index 0000000000..49c8a1e9af --- /dev/null +++ b/source/test-java/org/alfresco/repo/domain/permissions/FixedAclUpdaterTest.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.domain.permissions; + +import java.util.HashSet; +import java.util.Set; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.domain.node.NodeDAO.NodeRefQueryCallback; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.impl.PermissionsDaoComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.TransactionListenerAdapter; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.ArgumentHelper; +import org.alfresco.util.Pair; +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Test class for {@link FixedAclUpdater} + * + * @author Andreea Dragoi + * @since 4.2.7 + * + */ +public class FixedAclUpdaterTest extends TestCase +{ + private ApplicationContext ctx; + private RetryingTransactionHelper txnHelper; + private FileFolderService fileFolderService; + private Repository repository; + private FixedAclUpdater fixedAclUpdater; + private NodeRef folderNodeRef; + private PermissionsDaoComponent permissionsDaoComponent; + private NodeDAO nodeDAO; + + @Override + public void setUp() throws Exception + { + ctx = ApplicationContextHelper.getApplicationContext(); + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + txnHelper = serviceRegistry.getTransactionService().getRetryingTransactionHelper(); + fileFolderService = serviceRegistry.getFileFolderService(); + repository = (Repository) ctx.getBean("repositoryHelper"); + fixedAclUpdater = (FixedAclUpdater) ctx.getBean("fixedAclUpdater"); + permissionsDaoComponent = (PermissionsDaoComponent) ctx.getBean("admPermissionsDaoComponent"); + nodeDAO = (NodeDAO) ctx.getBean("nodeDAO"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + NodeRef home = repository.getCompanyHome(); + // create a folder hierarchy for which will change permission inheritance + int[] filesPerLevel = { 5, 5, 10 }; + RetryingTransactionCallback cb = createFolderHierchyCallback(home, fileFolderService, "ROOT", filesPerLevel); + folderNodeRef = txnHelper.doInTransaction(cb); + + // change setFixedAclMaxTransactionTime to lower value so setInheritParentPermissions on created folder hierarchy require async call + setFixedAclMaxTransactionTime(permissionsDaoComponent, home, 50); + + } + + private static void setFixedAclMaxTransactionTime(PermissionsDaoComponent permissionsDaoComponent, NodeRef folderNodeRef, + long fixedAclMaxTransactionTime) + { + if (permissionsDaoComponent instanceof ADMPermissionsDaoComponentImpl) + { + AccessControlListDAO acldao = ((ADMPermissionsDaoComponentImpl) permissionsDaoComponent).getACLDAO(folderNodeRef); + if (acldao instanceof ADMAccessControlListDAO) + { + ADMAccessControlListDAO admAcLDao = (ADMAccessControlListDAO) acldao; + admAcLDao.setFixedAclMaxTransactionTime(fixedAclMaxTransactionTime); + } + } + } + + private static RetryingTransactionCallback createFolderHierchyCallback(final NodeRef root, + final FileFolderService fileFolderService, final String rootName, final int[] filesPerLevel) + { + RetryingTransactionCallback cb = new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + NodeRef parent = createFile(fileFolderService, root, rootName, ContentModel.TYPE_FOLDER); + createFolderHierchy(fileFolderService, parent, 0, filesPerLevel); + return parent; + } + }; + return cb; + } + + @Override + public void tearDown() throws Exception + { + // delete created folder hierarchy + try + { + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + Set aspect = new HashSet<>(); + aspect.add(ContentModel.ASPECT_TEMPORARY); + nodeDAO.addNodeAspects(nodeDAO.getNodePair(folderNodeRef).getFirst(), aspect); + fileFolderService.delete(folderNodeRef); + return null; + } + }); + } + catch (Exception e) + { + } + AuthenticationUtil.clearCurrentSecurityContext(); + } + + private static NodeRef createFile(FileFolderService fileFolderService, NodeRef parent, String name, QName type) + { + return fileFolderService.create(parent, name + "_" + System.currentTimeMillis(), type).getNodeRef(); + } + + /** + * Get number of nodes with ASPECT_PENDING_FIX_ACL + */ + private int getNodesCountWithPendingFixedAclAspect() + { + final Set aspects = new HashSet<>(1); + aspects.add(ContentModel.ASPECT_PENDING_FIX_ACL); + GetNodesCountWithAspectCallback callback = new GetNodesCountWithAspectCallback(); + nodeDAO.getNodesWithAspects(aspects, 1L, null, callback); + return callback.getNodesNumber(); + } + + @Test + public void testMNT15368() + { + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // call setInheritParentPermissions on a node that will required async + AlfrescoTransactionSupport.bindResource(FixedAclUpdater.FIXED_ACL_ASYNC_CALL_KEY, true); + permissionsDaoComponent.setInheritParentPermissions(folderNodeRef, false); + + Boolean asyncCallRequired = (Boolean) AlfrescoTransactionSupport.getResource(FixedAclUpdater.FIXED_ACL_ASYNC_REQUIRED_KEY); + if (asyncCallRequired != null && asyncCallRequired) + { + // check if there are nodes with ASPECT_PENDING_FIX_ACL + assertTrue(" No nodes with pending aspect", getNodesCountWithPendingFixedAclAspect() > 0); + AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter() + { + @Override + public void afterCommit() + { + // start fixedAclUpdater + Thread t = new Thread(new Runnable() + { + @Override + public void run() + { + fixedAclUpdater.execute(); + } + }); + t.start(); + try + { + // wait to finish work + t.join(); + } + catch (InterruptedException e) + { + } + } + }); + } + return null; + } + }); + // check if nodes with ASPECT_PENDING_FIX_ACL are processed + assertTrue("Not all nodes were processed", getNodesCountWithPendingFixedAclAspect() == 0); + + } + + private static class GetNodesCountWithAspectCallback implements NodeRefQueryCallback + { + int nodesNumber = 0; + + @Override + public boolean handle(Pair nodePair) + { + nodesNumber++; + return true; + } + + public int getNodesNumber() + { + return nodesNumber; + } + } + + /** + * Creates a level in folder/file hierarchy. Intermediate levels will + * contain folders and last ones files + * + * @param fileFolderService + * @param parent + * - parent node of the of hierarchy level + * @param level + * - zero based + * @param filesPerLevel + * - array containing number of folders/files per level + */ + private static void createFolderHierchy(FileFolderService fileFolderService, NodeRef parent, int level, int[] filesPerLevel) + { + int levels = filesPerLevel.length; + // intermediate level + if (level < levels - 1) + { + int numFiles = filesPerLevel[level]; + for (int i = 0; i < numFiles; i++) + { + NodeRef node = createFile(fileFolderService, parent, "LVL" + level + i, ContentModel.TYPE_FOLDER); + createFolderHierchy(fileFolderService, node, level + 1, filesPerLevel); + } + } + // last level + else if (level == levels - 1) + { + int numFiles = filesPerLevel[level]; + for (int i = 0; i < numFiles; i++) + { + createFile(fileFolderService, parent, "File" + i, ContentModel.TYPE_CONTENT); + } + } + } + + /** + * Create a folder hierarchy and start FixedAclUpdater. See {@link #getUsage()} for usage parameters + */ + public static void main(String... args) + { + ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) ApplicationContextHelper.getApplicationContext(); + try + { + run(ctx, args); + } + catch (Exception e) + { + System.out.println("Failed to run FixedAclUpdaterTest test"); + e.printStackTrace(); + } + finally + { + ctx.close(); + } + } + + public static void run(final ApplicationContext ctx, String... args) throws InterruptedException + { + ArgumentHelper argHelper = new ArgumentHelper(getUsage(), args); + int threadCount = argHelper.getIntegerValue("threads", true, 1, 100); + String levels[] = argHelper.getStringValue("filesPerLevel", true, true).split(","); + int fixedAclMaxTransactionTime = argHelper.getIntegerValue("fixedAclMaxTransactionTime", true, 1, 10000); + final int[] filesPerLevel = new int[levels.length]; + for (int i = 0; i < levels.length; i++) + { + filesPerLevel[i] = Integer.parseInt(levels[i]); + } + + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + final RetryingTransactionHelper txnHelper = serviceRegistry.getTransactionService().getRetryingTransactionHelper(); + final FileFolderService fileFolderService = serviceRegistry.getFileFolderService(); + Repository repository = (Repository) ctx.getBean("repositoryHelper"); + final FixedAclUpdater fixedAclUpdater = (FixedAclUpdater) ctx.getBean("fixedAclUpdater"); + final PermissionsDaoComponent permissionsDaoComponent = (PermissionsDaoComponent) ctx.getBean("admPermissionsDaoComponent"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + NodeRef home = repository.getCompanyHome(); + final NodeRef root = createFile(fileFolderService, home, "ROOT", ContentModel.TYPE_FOLDER); + + // create a folder hierarchy for which will change permission inheritance + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) + { + final int index = i; + Thread t = new Thread(new Runnable() + { + @Override + public void run() + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + + RetryingTransactionCallback cb = createFolderHierchyCallback(root, fileFolderService, "FOLDER" + index, filesPerLevel); + txnHelper.doInTransaction(cb); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + }); + t.start(); + threads[i] = t; + } + for (int i = 0; i < threads.length; i++) + { + threads[i].join(); + } + + setFixedAclMaxTransactionTime(permissionsDaoComponent, home, fixedAclMaxTransactionTime); + + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // call setInheritParentPermissions on a node that will required async + AlfrescoTransactionSupport.bindResource(FixedAclUpdater.FIXED_ACL_ASYNC_CALL_KEY, true); + final long startTime = System.currentTimeMillis(); + permissionsDaoComponent.setInheritParentPermissions(root, false); + + Boolean asyncCallRequired = (Boolean) AlfrescoTransactionSupport.getResource(FixedAclUpdater.FIXED_ACL_ASYNC_REQUIRED_KEY); + if (asyncCallRequired != null && asyncCallRequired) + { + // check if there are nodes with ASPECT_PENDING_FIX_ACL + AlfrescoTransactionSupport.bindListener(new TransactionListenerAdapter() + { + @Override + public void afterCommit() + { + long userEndTime = System.currentTimeMillis(); + // start fixedAclUpdater + Thread t = new Thread(new Runnable() + { + @Override + public void run() + { + fixedAclUpdater.execute(); + } + }); + t.start(); + try + { + // wait to finish work + t.join(); + System.out.println("Backend time " + (System.currentTimeMillis() - startTime)); + System.out.println("User time " + (userEndTime - startTime)); + } + catch (InterruptedException e) + { + } + } + }); + } + return null; + } + }, false, true); + } + + private static String getUsage() + { + StringBuilder sb = new StringBuilder(); + sb.append("FixedAclUpdaterTest usage: ").append("\n"); + sb.append(" FixedAclUpdaterTest --threads= --fixedAclMaxTransactionTime= --filesPerLevel=").append("\n"); + sb.append(" maxtime: max transaction time for fixed acl ").append("\n"); + sb.append(" threadcount: number of threads to create the folder hierarchy ").append("\n"); + sb.append(" levelfiles: number of folders/files per level separated by comma").append("\n"); + return sb.toString(); + } +} diff --git a/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java b/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java index c41646363d..0f5461302e 100644 --- a/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java +++ b/source/test-java/org/alfresco/repo/security/SecurityTestSuite.java @@ -22,6 +22,7 @@ import junit.framework.JUnit4TestAdapter; import junit.framework.Test; import junit.framework.TestSuite; +import org.alfresco.repo.domain.permissions.FixedAclUpdaterTest; import org.alfresco.repo.ownable.impl.OwnableServiceTest; import org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactoryTest; import org.alfresco.repo.security.authentication.AuthenticationBootstrapTest; @@ -85,6 +86,8 @@ public class SecurityTestSuite extends TestSuite suite.addTest(new JUnit4TestAdapter(HomeFolderProviderSynchronizerTest.class)); suite.addTest(new JUnit4TestAdapter(AlfrescoSSLSocketFactoryTest.class)); + suite.addTestSuite(FixedAclUpdaterTest.class); + return suite; } }