[ACS-12679] Implement mechanism to cleanup ACLs (#4384)

This commit is contained in:
Adam Decatur
2026-09-11 13:06:44 -05:00
committed by GitHub
parent 35b5eec92a
commit 9dea903bab
13 changed files with 597 additions and 0 deletions
@@ -35,6 +35,7 @@ import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.extensions.surf.util.ParameterCheck;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.cache.TransactionalCache;
import org.alfresco.repo.cache.lookup.EntityLookupCache;
@@ -42,6 +43,7 @@ import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAO;
import org.alfresco.repo.domain.CrcHelper;
import org.alfresco.repo.domain.qname.QNameDAO;
import org.alfresco.repo.security.permissions.ACEType;
import org.alfresco.repo.security.permissions.ACLType;
import org.alfresco.repo.security.permissions.PermissionReference;
import org.alfresco.repo.security.permissions.impl.SimplePermissionReference;
import org.alfresco.service.cmr.security.AccessStatus;
@@ -252,6 +254,64 @@ public abstract class AbstractAclCrudDAOImpl implements AclCrudDAO
return getLatestAclEntityByGuid(aclGuid);
}
@Override
public List<Long> getUnusedAclIds(long afterAclId, int maxResults)
{
if (maxResults < 1)
{
throw new IllegalArgumentException("maxResults must be greater than zero");
}
Long sharedAclToReplaceQNameId = getQNameId(ContentModel.PROP_SHARED_ACL_TO_REPLACE);
Long inheritFromAclQNameId = getQNameId(ContentModel.PROP_INHERIT_FROM_ACL);
return getUnusedAclEntityIds(afterAclId, sharedAclToReplaceQNameId, inheritFromAclQNameId,
ACLType.FIXED.getId(), ACLType.GLOBAL.getId(), maxResults);
}
@Override
public boolean deleteUnusedAcl(long aclEntityId)
{
Long sharedAclToReplaceQNameId = getQNameId(ContentModel.PROP_SHARED_ACL_TO_REPLACE);
Long inheritFromAclQNameId = getQNameId(ContentModel.PROP_INHERIT_FROM_ACL);
if (!isAclEntityUnused(aclEntityId, sharedAclToReplaceQNameId, inheritFromAclQNameId,
ACLType.FIXED.getId(), ACLType.GLOBAL.getId()))
{
return false;
}
Acl acl = getAcl(aclEntityId);
if (acl == null)
{
return false;
}
List<AclMember> members = getAclMembersByAcl(aclEntityId);
List<Long> aceIds = new ArrayList<>(members.size());
for (AclMember member : members)
{
aceIds.add(member.getAceId());
}
deleteAclMembersByAcl(aclEntityId);
deleteAcl(aclEntityId);
for (Long aceId : aceIds)
{
deleteAceEntityIfUnused(aceId);
}
if (acl.getAclChangeSetId() != null)
{
deleteAclChangeSetEntityIfUnused(acl.getAclChangeSetId());
}
return true;
}
private Long getQNameId(QName qname)
{
Pair<Long, QName> qnamePair = qnameDAO.getQName(qname);
return qnamePair == null ? null : qnamePair.getFirst();
}
public List<Long> getADMNodesByAcl(long aclEntityId, int maxResults)
{
return getADMNodeEntityIdsByAcl(aclEntityId, maxResults);
@@ -373,12 +433,20 @@ public abstract class AbstractAclCrudDAOImpl implements AclCrudDAO
protected abstract Long getLatestAclEntityByGuid(String aclGuid);
protected abstract List<Long> getUnusedAclEntityIds(long afterAclId, Long sharedAclToReplaceQNameId, Long inheritFromAclQNameId,
int fixedAclType, int globalAclType, int maxResults);
protected abstract boolean isAclEntityUnused(long aclEntityId, Long sharedAclToReplaceQNameId, Long inheritFromAclQNameId,
int fixedAclType, int globalAclType);
protected abstract int updateAclEntity(AclEntity entity);
protected abstract int updateAceEntity(AceEntity updatedAceEntity);
protected abstract int deleteAclEntity(long id);
protected abstract int deleteAclChangeSetEntityIfUnused(long aclChangeSetEntityId);
protected abstract List<Long> getADMNodeEntityIdsByAcl(long aclEntityId, int maxResults);
//
@@ -681,6 +749,8 @@ public abstract class AbstractAclCrudDAOImpl implements AclCrudDAO
protected abstract int deleteAceEntities(List<Long> aceIds);
protected abstract int deleteAceEntityIfUnused(long aceId);
//
// Permission
//
@@ -63,6 +63,10 @@ public interface AclCrudDAO
public Long getLatestAclByGuid(String aclGuid);
List<Long> getUnusedAclIds(long afterAclId, int maxResults);
boolean deleteUnusedAcl(long aclEntityId);
public void updateAcl(AclUpdateEntity entity);
public void deleteAcl(long aclEntityId);
@@ -0,0 +1,105 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.domain.permissions;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.PropertyCheck;
/**
* Removes a bounded batch of ACLs that have no repository references.
*/
public class UnusedAclCleaner
{
private static final Logger LOGGER = LoggerFactory.getLogger(UnusedAclCleaner.class);
private AclCrudDAO aclCrudDAO;
private TransactionService transactionService;
private int batchSize = 1000;
private boolean enabled = true;
public void setAclCrudDAO(AclCrudDAO aclCrudDAO)
{
this.aclCrudDAO = aclCrudDAO;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setBatchSize(int batchSize)
{
if (batchSize < 1)
{
throw new IllegalArgumentException("batchSize must be greater than zero");
}
this.batchSize = batchSize;
}
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
public void init()
{
PropertyCheck.mandatory(this, "aclCrudDAO", aclCrudDAO);
PropertyCheck.mandatory(this, "transactionService", transactionService);
}
public int execute()
{
if (!enabled)
{
LOGGER.debug("Unused ACL cleanup is disabled.");
return 0;
}
int deleted = transactionService.getRetryingTransactionHelper()
.doInTransaction(this::cleanupBatch, false, true);
LOGGER.info("Unused ACL cleanup removed {} ACLs.", deleted);
return deleted;
}
int cleanupBatch()
{
List<Long> aclIds = aclCrudDAO.getUnusedAclIds(0, batchSize);
int deleted = 0;
for (Long aclId : aclIds)
{
if (aclCrudDAO.deleteUnusedAcl(aclId))
{
deleted++;
}
}
return deleted;
}
}
@@ -0,0 +1,51 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.domain.permissions;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.schedule.AbstractScheduledLockedJob;
/**
* Cluster-safe scheduled entry point for unused ACL cleanup.
*/
public class UnusedAclCleanupJob extends AbstractScheduledLockedJob
{
@Override
public void executeJob(JobExecutionContext context) throws JobExecutionException
{
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
Object cleaner = jobDataMap.get("unusedAclCleaner");
if (!(cleaner instanceof UnusedAclCleaner))
{
throw new AlfrescoRuntimeException("UnusedAclCleanupJob must contain a valid 'unusedAclCleaner'");
}
((UnusedAclCleaner) cleaner).execute();
}
}
@@ -59,6 +59,8 @@ public class AclCrudDAOImpl extends AbstractAclCrudDAOImpl
private static final String SELECT_ACL_BY_ID = "alfresco.permissions.select_AclById";
private static final String SELECT_ACLS_THAT_INHERIT_FROM_ACL = "alfresco.permissions.select_AclsThatInheritFromAcl";
private static final String SELECT_LATEST_ACL_BY_GUID = "alfresco.permissions.select_LatestAclByGuid";
private static final String SELECT_UNUSED_ACL_IDS = "alfresco.permissions.select_UnusedAclIds";
private static final String SELECT_IS_ACL_UNUSED = "alfresco.permissions.select_IsAclUnused";
private static final String SELECT_ADM_NODES_BY_ACL = "alfresco.permissions.select_ADMNodesByAclId";
private static final String UPDATE_ACL = "alfresco.permissions.update_Acl";
private static final String DELETE_ACL = "alfresco.permissions.delete_Acl";
@@ -75,6 +77,7 @@ public class AclCrudDAOImpl extends AbstractAclCrudDAOImpl
private static final String UPDATE_ACL_CHANGESET = "alfresco.permissions.update_AclChangeSet";
private static final String SELECT_ACL_CHANGESET_BY_ID = "alfresco.permissions.select_AclChangeSetById";
private static final String DELETE_ACL_CHANGESET = "alfresco.permissions.delete_AclChangeSet";
private static final String DELETE_UNUSED_ACL_CHANGESET = "alfresco.permissions.delete_UnusedAclChangeSet";
private static final String INSERT_ACE = "alfresco.permissions.insert.insert_Ace";
private static final String SELECT_ACE_BY_ID = "alfresco.permissions.select_AceById";
@@ -82,6 +85,7 @@ public class AclCrudDAOImpl extends AbstractAclCrudDAOImpl
private static final String SELECT_ACES_AND_AUTHORIES_BY_ACL = "alfresco.permissions.select_AcesAndAuthoritiesByAclId";
private static final String SELECT_ACE_WITH_NO_CONTEXT = "alfresco.permissions.select_AceWithNoContext";
private static final String DELETE_ACES_LIST = "alfresco.permissions.delete_AcesList";
private static final String DELETE_UNUSED_ACE = "alfresco.permissions.delete_UnusedAce";
private static final String UPDATE_ACE = "alfresco.permissions.update_Ace";
private static final String INSERT_ACE_CONTEXT = "alfresco.permissions.insert.insert_AceContext";
@@ -151,6 +155,33 @@ public class AclCrudDAOImpl extends AbstractAclCrudDAOImpl
return template.selectOne(SELECT_LATEST_ACL_BY_GUID, params);
}
@Override
protected List<Long> getUnusedAclEntityIds(long afterAclId, Long sharedAclToReplaceQNameId, Long inheritFromAclQNameId,
int fixedAclType, int globalAclType, int maxResults)
{
Map<String, Object> params = new HashMap<>(5);
params.put("afterAclId", afterAclId);
params.put("sharedAclToReplaceQNameId", sharedAclToReplaceQNameId);
params.put("inheritFromAclQNameId", inheritFromAclQNameId);
params.put("fixedAclType", fixedAclType);
params.put("globalAclType", globalAclType);
return template.selectList(SELECT_UNUSED_ACL_IDS, params, new RowBounds(0, maxResults));
}
@Override
protected boolean isAclEntityUnused(long aclEntityId, Long sharedAclToReplaceQNameId, Long inheritFromAclQNameId,
int fixedAclType, int globalAclType)
{
Map<String, Object> params = new HashMap<>(5);
params.put("id", aclEntityId);
params.put("sharedAclToReplaceQNameId", sharedAclToReplaceQNameId);
params.put("inheritFromAclQNameId", inheritFromAclQNameId);
params.put("fixedAclType", fixedAclType);
params.put("globalAclType", globalAclType);
return template.selectOne(SELECT_IS_ACL_UNUSED, params) != null;
}
@SuppressWarnings("unchecked")
@Override
protected List<Long> getADMNodeEntityIdsByAcl(long aclEntityId, int maxResults)
@@ -284,6 +315,14 @@ public class AclCrudDAOImpl extends AbstractAclCrudDAOImpl
return template.delete(DELETE_ACL_CHANGESET, params);
}
@Override
protected int deleteAclChangeSetEntityIfUnused(long aclChangeSetEntityId)
{
Map<String, Object> params = new HashMap<>(1);
params.put("id", aclChangeSetEntityId);
return template.delete(DELETE_UNUSED_ACL_CHANGESET, params);
}
@Override
protected int updateChangeSetEntity(Long id, long commitTimeMs)
{
@@ -361,6 +400,14 @@ public class AclCrudDAOImpl extends AbstractAclCrudDAOImpl
return template.delete(DELETE_ACES_LIST, aceEntityIds);
}
@Override
protected int deleteAceEntityIfUnused(long aceEntityId)
{
Map<String, Object> params = new HashMap<>(1);
params.put("id", aceEntityId);
return template.delete(DELETE_UNUSED_ACE, params);
}
@Override
protected long createAceContextEntity(AceContextEntity entity)
{
@@ -385,6 +385,52 @@
acl.acl_id = ? and acl.latest = ?
</select>
<select id="select_UnusedAclIds" parameterType="map" resultType="long">
select
acl.id
from
alf_access_control_list acl
where
acl.id &gt; #{afterAclId,jdbcType=BIGINT} and
acl.type not in (#{fixedAclType,jdbcType=INTEGER}, #{globalAclType,jdbcType=INTEGER}) and
not exists (select 1 from alf_node node where node.acl_id = acl.id) and
not exists (select 1 from alf_access_control_list inherited_acl where inherited_acl.inherited_acl = acl.id) and
not exists (select 1 from alf_access_control_list parent_acl where parent_acl.inherits_from = acl.id) and
not exists (
select 1
from alf_node_properties node_property
where node_property.long_value = acl.id and
node_property.qname_id in (
#{sharedAclToReplaceQNameId,jdbcType=BIGINT},
#{inheritFromAclQNameId,jdbcType=BIGINT}
)
)
order by
acl.id
</select>
<select id="select_IsAclUnused" parameterType="map" resultType="long">
select
acl.id
from
alf_access_control_list acl
where
acl.id = #{id,jdbcType=BIGINT} and
acl.type not in (#{fixedAclType,jdbcType=INTEGER}, #{globalAclType,jdbcType=INTEGER}) and
not exists (select 1 from alf_node node where node.acl_id = acl.id) and
not exists (select 1 from alf_access_control_list inherited_acl where inherited_acl.inherited_acl = acl.id) and
not exists (select 1 from alf_access_control_list parent_acl where parent_acl.inherits_from = acl.id) and
not exists (
select 1
from alf_node_properties node_property
where node_property.long_value = acl.id and
node_property.qname_id in (
#{sharedAclToReplaceQNameId,jdbcType=BIGINT},
#{inheritFromAclQNameId,jdbcType=BIGINT}
)
)
</select>
<select id="select_ADMNodesByAclId" parameterMap="parameter_IdMap" resultType="long">
select
id
@@ -578,6 +624,15 @@
</foreach>
</delete>
<delete id="delete_UnusedAce" parameterMap="parameter_IdMap">
delete
from
alf_access_control_entry
where
id = ? and
not exists (select 1 from alf_acl_member member where member.ace_id = alf_access_control_entry.id)
</delete>
<!-- for patch -->
<delete id="delete_UnusedAces">
delete
@@ -632,6 +687,15 @@
id = ?
</delete>
<delete id="delete_UnusedAclChangeSet" parameterMap="parameter_IdMap">
delete
from
alf_acl_change_set
where
id = ? and
not exists (select 1 from alf_access_control_list acl where acl.acl_change_set = alf_acl_change_set.id)
</delete>
<delete id="delete_AceContext" parameterMap="parameter_IdMap">
delete
from
@@ -128,6 +128,13 @@
<property name="policyIgnoreUtil" ref="policyIgnoreUtil"/>
</bean>
<bean id="unusedAclCleaner" class="org.alfresco.repo.domain.permissions.UnusedAclCleaner" init-method="init">
<property name="aclCrudDAO" ref="aclCrudDAO"/>
<property name="transactionService" ref="transactionService"/>
<property name="enabled" value="${system.aclCleanup.enabled}"/>
<property name="batchSize" value="${system.aclCleanup.batchSize}"/>
</bean>
<!-- =================== -->
<!-- Dynamic Authorities -->
<!-- =================== -->
@@ -1141,6 +1141,11 @@ system.fixedACLsUpdater.maxItems=-1
# fixedACLsUpdater - Impose the order by in the query. If false, it may not process all the results but should do the queries faster
system.fixedACLsUpdater.orderNodes=true
# Unused ACL cleanup - enabled by default and limited to one batch per daily execution
system.aclCleanup.enabled=true
system.aclCleanup.cronExpression=0 0 2 * * ?
system.aclCleanup.batchSize=1000
cmis.disable.hidden.leading.period.files=false
#Smart Folders Config Properties
@@ -27,6 +27,7 @@
<ref bean="upgradePasswordHashJobTrigger"/>
<ref bean="patchAddUnmovableAspectTrigger"/>
<ref bean="fixedAclUpdaterTrigger"/>
<ref bean="unusedAclCleanupTrigger"/>
</list>
</property>
</bean>
@@ -227,4 +228,21 @@
</property>
</bean>
<bean id="unusedAclCleanupTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="cronExpression" value="${system.aclCleanup.cronExpression}"/>
<property name="startDelay" value="${system.cronJob.startDelayMilliseconds}"/>
<property name="jobDetail">
<bean id="unusedAclCleanupJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="org.alfresco.repo.domain.permissions.UnusedAclCleanupJob"/>
<property name="jobDataAsMap">
<map>
<entry key="unusedAclCleaner" value-ref="unusedAclCleaner"/>
<entry key="jobLockService" value-ref="jobLockService"/>
<entry key="name" value="unusedAclCleanupJob"/>
</map>
</property>
</bean>
</property>
</bean>
</beans>
@@ -239,6 +239,8 @@ import org.alfresco.util.testing.category.NonBuildTests;
org.alfresco.repo.content.caching.CachingContentStoreTest.class,
org.alfresco.repo.content.caching.ContentCacheImplTest.class,
org.alfresco.repo.domain.permissions.FixedAclUpdaterUnitTest.class,
org.alfresco.repo.domain.permissions.UnusedAclCleanerTest.class,
org.alfresco.repo.domain.permissions.UnusedAclCleanupJobTest.class,
org.alfresco.repo.domain.propval.PropertyTypeConverterTest.class,
org.alfresco.repo.domain.schema.script.ScriptBundleExecutorImplTest.class,
org.alfresco.repo.search.MLAnaysisModeExpansionTest.class,
@@ -26,17 +26,25 @@
package org.alfresco.repo.domain.permissions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import junit.framework.TestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.springframework.context.ApplicationContext;
import org.alfresco.repo.security.permissions.ACEType;
import org.alfresco.repo.security.permissions.ACLType;
import org.alfresco.repo.security.permissions.PermissionReference;
import org.alfresco.repo.security.permissions.SimpleAccessControlListProperties;
import org.alfresco.repo.security.permissions.impl.SimplePermissionReference;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.test_category.OwnJVMTestsCategory;
@@ -57,7 +65,9 @@ public class AclCrudDAOTest extends TestCase
private TransactionService transactionService;
private RetryingTransactionHelper txnHelper;
private AclCrudDAO aclCrudDAO;
private AclDAO aclDAO;
@Before
@Override
public void setUp() throws Exception
{
@@ -66,6 +76,75 @@ public class AclCrudDAOTest extends TestCase
txnHelper = transactionService.getRetryingTransactionHelper();
aclCrudDAO = (AclCrudDAO) ctx.getBean("aclCrudDAO");
aclDAO = (AclDAO) ctx.getBean("aclDAO");
}
@Test
public void testGetUnusedAclIdsReturnsBoundedIsolatedAcls() throws Exception
{
List<Long> aclIds = txnHelper.doInTransaction(() -> {
SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties();
properties.setAclType(ACLType.DEFINING);
properties.setInherits(true);
properties.setVersioned(false);
Long definingAclId = aclDAO.createAccessControlList(properties).getId();
Long sharedAclId = aclDAO.getInheritedAccessControlList(definingAclId);
properties.setInherits(false);
properties.setAclType(ACLType.FIXED);
aclDAO.createAccessControlList(properties);
properties.setAclType(ACLType.GLOBAL);
aclDAO.createAccessControlList(properties);
properties.setAclType(ACLType.DEFINING);
Long firstUnusedAclId = aclDAO.createAccessControlList(properties).getId();
Long secondUnusedAclId = aclDAO.createAccessControlList(properties).getId();
return Arrays.asList(sharedAclId, firstUnusedAclId, secondUnusedAclId);
});
List<Long> unusedAclIds = txnHelper.doInTransaction(() -> aclCrudDAO.getUnusedAclIds(aclIds.get(0), 1), true);
assertEquals(1, unusedAclIds.size());
assertEquals(aclIds.get(1), unusedAclIds.get(0));
}
@Test
public void testDeleteUnusedAclPreservesSharedAceUntilLastReferenceIsRemoved() throws Exception
{
List<Long> ids = txnHelper.doInTransaction(() -> {
SimpleAccessControlListProperties properties = new SimpleAccessControlListProperties();
properties.setAclType(ACLType.DEFINING);
properties.setInherits(false);
properties.setVersioned(false);
Long firstAclId = aclDAO.createAccessControlList(properties).getId();
Long secondAclId = aclDAO.createAccessControlList(properties).getId();
Authority authority = aclCrudDAO.getOrCreateAuthority("acl-cleanup-test-user");
Permission permission = aclCrudDAO.getOrCreatePermission(
SimplePermissionReference.getPermissionReference(
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "aclCleanupTest"), "Read"));
Ace ace = aclCrudDAO.getOrCreateAce(permission, authority, ACEType.ALL, AccessStatus.ALLOWED);
aclCrudDAO.addAclMembersToAcl(firstAclId, Arrays.asList(ace.getId()), 0);
aclCrudDAO.addAclMembersToAcl(secondAclId, Arrays.asList(ace.getId()), 0);
return Arrays.asList(firstAclId, secondAclId, ace.getId(), aclCrudDAO.getAcl(firstAclId).getAclChangeSetId());
});
assertTrue(txnHelper.doInTransaction(() -> aclCrudDAO.deleteUnusedAcl(ids.get(0))));
txnHelper.doInTransaction(() -> {
assertNull(aclCrudDAO.getAcl(ids.get(0)));
assertNotNull(aclCrudDAO.getAce(ids.get(2)));
assertNotNull(aclCrudDAO.getAclChangeSet(ids.get(3)));
return null;
}, true);
assertTrue(txnHelper.doInTransaction(() -> aclCrudDAO.deleteUnusedAcl(ids.get(1))));
txnHelper.doInTransaction(() -> {
assertNull(aclCrudDAO.getAcl(ids.get(1)));
assertNull(aclCrudDAO.getAce(ids.get(2)));
assertNull(aclCrudDAO.getAclChangeSet(ids.get(3)));
return null;
}, true);
}
// TODO - alf_access_control_list, alf_acl_member, alf_access_control_entry
@@ -0,0 +1,76 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.domain.permissions;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import org.junit.Test;
import org.alfresco.service.transaction.TransactionService;
public class UnusedAclCleanerTest
{
private final AclCrudDAO aclCrudDAO = mock(AclCrudDAO.class);
@Test
public void cleanupBatchDeletesAtMostConfiguredCandidates()
{
UnusedAclCleaner cleaner = new UnusedAclCleaner();
cleaner.setAclCrudDAO(aclCrudDAO);
cleaner.setBatchSize(2);
when(aclCrudDAO.getUnusedAclIds(0, 2)).thenReturn(Arrays.asList(11L, 12L));
when(aclCrudDAO.deleteUnusedAcl(11L)).thenReturn(true);
when(aclCrudDAO.deleteUnusedAcl(12L)).thenReturn(false);
assertEquals(1, cleaner.cleanupBatch());
verify(aclCrudDAO).deleteUnusedAcl(11L);
verify(aclCrudDAO).deleteUnusedAcl(12L);
}
@Test
public void disabledCleanerDoesNothing()
{
UnusedAclCleaner cleaner = new UnusedAclCleaner();
cleaner.setAclCrudDAO(aclCrudDAO);
cleaner.setTransactionService(mock(TransactionService.class));
cleaner.setEnabled(false);
assertEquals(0, cleaner.execute());
verifyNoInteractions(aclCrudDAO);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsInvalidBatchSize()
{
new UnusedAclCleaner().setBatchSize(0);
}
}
@@ -0,0 +1,69 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.domain.permissions;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.alfresco.error.AlfrescoRuntimeException;
public class UnusedAclCleanupJobTest
{
private final JobExecutionContext context = mock(JobExecutionContext.class);
private final UnusedAclCleaner cleaner = mock(UnusedAclCleaner.class);
private JobDetail jobDetail;
@Before
public void setUp()
{
jobDetail = JobBuilder.newJob(UnusedAclCleanupJob.class).withIdentity("unusedAclCleanupJob").build();
jobDetail.getJobDataMap().put("unusedAclCleaner", cleaner);
when(context.getJobDetail()).thenReturn(jobDetail);
}
@Test
public void delegatesCleanup() throws JobExecutionException
{
new UnusedAclCleanupJob().executeJob(context);
verify(cleaner).execute();
}
@Test(expected = AlfrescoRuntimeException.class)
public void rejectsMissingCleaner() throws JobExecutionException
{
jobDetail.getJobDataMap().remove("unusedAclCleaner");
new UnusedAclCleanupJob().executeJob(context);
}
}