[MNT-25707] Audit disabled paths cache + Property value cache allowEqualsChecks configurable (#4062) (#4086)

This commit is contained in:
Tiago Salvado
2026-05-18 12:40:57 +01:00
committed by GitHub
parent faed0912e6
commit ecaaa17c84
9 changed files with 337 additions and 8 deletions
+3 -3
View File
@@ -1242,7 +1242,7 @@
"filename": "repository/src/main/resources/alfresco/repository.properties",
"hashed_secret": "1459a56410378e4d3ab470eff570e5eae1742762",
"is_verified": false,
"line_number": 318,
"line_number": 320,
"is_secret": false
},
{
@@ -1250,7 +1250,7 @@
"filename": "repository/src/main/resources/alfresco/repository.properties",
"hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d",
"is_verified": false,
"line_number": 777,
"line_number": 779,
"is_secret": false
}
],
@@ -1845,5 +1845,5 @@
}
]
},
"generated_at": "2026-02-25T12:24:46Z"
"generated_at": "2026-05-07T12:42:51Z"
}
@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* Copyright (C) 2005 - 2026 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -45,6 +45,7 @@ import org.alfresco.repo.audit.model.AuditApplication;
import org.alfresco.repo.audit.model.AuditApplication.DataExtractorDefinition;
import org.alfresco.repo.audit.model.AuditModelRegistry;
import org.alfresco.repo.audit.model.AuditModelRegistryImpl;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.domain.audit.AuditDAO;
import org.alfresco.repo.domain.propval.PropertyValueDAO;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
@@ -82,6 +83,8 @@ public class AuditComponentImpl implements AuditComponent
private AuditFilter auditFilter;
private UserAuditFilter userAuditFilter;
private AuditRecordReporter auditRecordReporter;
private SimpleCache<Long, Set<String>> disabledPathsCache;
private boolean disabledPathsCacheEnabled;
/**
* Default constructor
@@ -145,6 +148,16 @@ public class AuditComponentImpl implements AuditComponent
this.auditRecordReporter = auditRecordReporter;
}
public void setDisabledPathsCache(SimpleCache<Long, Set<String>> disabledPathsCache)
{
this.disabledPathsCache = disabledPathsCache;
}
public void setDisabledPathsCacheEnabled(boolean disabledPathsCacheEnabled)
{
this.disabledPathsCacheEnabled = disabledPathsCacheEnabled;
}
/**
* {@inheritDoc}
*
@@ -232,13 +245,12 @@ public class AuditComponentImpl implements AuditComponent
* the audit application object
* @return Returns a copy of the set of disabled paths associated with the application
*/
@SuppressWarnings("unchecked")
private Set<String> getDisabledPaths(AuditApplication application)
{
try
{
Long disabledPathsId = application.getDisabledPathsId();
Set<String> disabledPaths = (Set<String>) propertyValueDAO.getPropertyById(disabledPathsId);
Set<String> disabledPaths = getDisabledPaths(disabledPathsId);
return new HashSet<>(disabledPaths);
}
catch (Throwable e)
@@ -431,6 +443,9 @@ public class AuditComponentImpl implements AuditComponent
if (changed)
{
propertyValueDAO.updateProperty(disabledPathsId, (Serializable) disabledPaths);
removeCachedDisabledPaths(disabledPathsId);
if (logger.isDebugEnabled())
{
logger.debug(
@@ -514,6 +529,9 @@ public class AuditComponentImpl implements AuditComponent
disabledPaths.add(path);
// Upload the new set
propertyValueDAO.updateProperty(disabledPathsId, (Serializable) disabledPaths);
removeCachedDisabledPaths(disabledPathsId);
// Done
if (logger.isDebugEnabled())
{
@@ -545,6 +563,9 @@ public class AuditComponentImpl implements AuditComponent
}
Long disabledPathsId = application.getDisabledPathsId();
propertyValueDAO.updateProperty(disabledPathsId, (Serializable) Collections.emptySet());
removeCachedDisabledPaths(disabledPathsId);
// Done
if (logger.isDebugEnabled())
{
@@ -999,4 +1020,35 @@ public class AuditComponentImpl implements AuditComponent
{
return auditDAO.getAuditEntriesCountByAppAndProperties(applicationName, parameters);
}
@SuppressWarnings("unchecked")
private Set<String> getDisabledPaths(Long disabledPathsId)
{
Set<String> disabledPaths = null;
if (disabledPathsCacheEnabled)
{
disabledPaths = disabledPathsCache.get(disabledPathsId);
}
if (disabledPaths == null)
{
disabledPaths = (Set<String>) propertyValueDAO.getPropertyById(disabledPathsId);
if (disabledPathsCacheEnabled)
{
disabledPathsCache.put(disabledPathsId, disabledPaths);
}
}
return disabledPaths;
}
private void removeCachedDisabledPaths(Long disabledPathsId)
{
if (disabledPathsCacheEnabled)
{
disabledPathsCache.remove(disabledPathsId);
}
}
}
@@ -38,6 +38,8 @@
</bean>
</property>
<property name="auditRecordReporter" ref="auditRecordReporter"/>
<property name="disabledPathsCache" ref="auditDisabledPathsCache"/>
<property name="disabledPathsCacheEnabled" value="${audit.disabled-paths-cache.enabled}" />
</bean>
<!-- User Audit Filter -->
@@ -520,4 +520,13 @@
<constructor-arg value="cache.folderSizeSharedCache"/>
</bean>
<!-- ===================================== -->
<!-- Audit Disabled Paths Cache -->
<!-- ===================================== -->
<!-- The cache for audit disabled paths -->
<bean name="auditDisabledPathsCache" factory-bean="cacheFactory" factory-method="createCache">
<constructor-arg value="cache.auditDisabledPathsCache"/>
</bean>
</beans>
@@ -63,6 +63,7 @@ cache.propertyValueCache.backup-count=1
cache.propertyValueCache.eviction-policy=LRU
cache.propertyValueCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.propertyValueCache.readBackupData=false
cache.propertyValueCache.tx.allowEqualsChecks=false
cache.propertyClassCache.tx.maxItems=1000
cache.propertyClassCache.tx.statsEnabled=${caches.tx.statsEnabled}
@@ -718,4 +719,20 @@ cache.folderSizeSharedCache.cluster.type=fully-distributed
cache.folderSizeSharedCache.backup-count=1
cache.folderSizeSharedCache.eviction-policy=LRU
cache.folderSizeSharedCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.folderSizeSharedCache.readBackupData=false
cache.folderSizeSharedCache.readBackupData=false
#
# Audit disabled paths cache
#
# Setting this cache cluster type as local prevents the write-invalidate
# storm caused by EntityLookupCache converting read-misses into cache
# puts on this stable, rarely-modified value. TTL bounds value staleness.
#
cache.auditDisabledPathsCache.maxItems=100
cache.auditDisabledPathsCache.timeToLiveSeconds=300
cache.auditDisabledPathsCache.maxIdleSeconds=0
cache.auditDisabledPathsCache.cluster.type=local
cache.auditDisabledPathsCache.backup-count=1
cache.auditDisabledPathsCache.eviction-policy=LRU
cache.auditDisabledPathsCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.auditDisabledPathsCache.readBackupData=false
@@ -300,6 +300,8 @@ audit.filter.alfresco-access.transaction.user=~System;~null;.*
audit.filter.alfresco-access.transaction.type=cm:folder;cm:content;st:site
audit.filter.alfresco-access.transaction.path=~/sys:archivedItem;~/ver:;.*
# Enable the disabled paths cache. This cache is used to store the disabled paths for each application to avoid hitting the database on every audited operation.
audit.disabled-paths-cache.enabled=true
# System Configuration
system.store=system://system
@@ -17,6 +17,7 @@
</property>
<property name="maxCacheSize" value="${cache.propertyValueCache.tx.maxItems}" />
<property name="mutable" value="true" />
<property name="allowEqualsChecks" value="${cache.propertyValueCache.tx.allowEqualsChecks}" />
<property name="disableSharedCache" value="${system.cache.disableMutableSharedCaches}" />
<property name="tenantAware" value="false" />
<property name="cacheStats" ref="cacheStatistics"/>
@@ -739,4 +740,3 @@
</bean>
</beans>
@@ -208,6 +208,7 @@ import org.alfresco.util.testing.category.NonBuildTests;
org.alfresco.repo.action.executer.TransformActionExecuterTest.class,
org.alfresco.repo.action.executer.ImporterActionExecutorUnitTest.class,
org.alfresco.repo.audit.AuditableAnnotationTest.class,
org.alfresco.repo.audit.AuditComponentImplUnitTest.class,
org.alfresco.repo.audit.PropertyAuditFilterTest.class,
org.alfresco.repo.audit.access.NodeChangeTest.class,
org.alfresco.repo.content.ContentServiceImplUnitTest.class,
@@ -0,0 +1,246 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2026 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.audit;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.MockitoAnnotations.openMocks;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.alfresco.repo.audit.model.AuditApplication;
import org.alfresco.repo.audit.model.AuditModelRegistryImpl;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.domain.audit.AuditDAO;
import org.alfresco.repo.domain.propval.PropertyValueDAO;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.service.transaction.TransactionService;
/**
* Unit tests for the disabled-paths cache behaviour in {@link AuditComponentImpl}.
*
* Covers cache-hit/miss on {@code getDisabledPaths} and cache-invalidation on {@code enableAudit}, {@code disableAudit}, and {@code resetDisabledPaths}.
*/
public class AuditComponentImplUnitTest
{
private static final String APPLICATION_NAME = "TestApp";
private static final Long DISABLED_PATHS_ID = 42L;
private static final String APP_KEY = "test-app";
@InjectMocks
private AuditComponentImpl auditComponent;
@Mock
private AuditModelRegistryImpl auditModelRegistry;
@Mock
private PropertyValueDAO propertyValueDAO;
@Mock
private AuditDAO auditDAO;
@Mock
private TransactionService transactionService;
@Mock
private AuditFilter auditFilter;
@Mock
private UserAuditFilter userAuditFilter;
@Mock
private AuditRecordReporter auditRecordReporter;
@Mock
private SimpleCache<Long, Set<String>> disabledPathsCache;
@Mock
private AuditApplication auditApplication;
private MockedStatic<AlfrescoTransactionSupport> mockedTxnSupport;
@Before
public void setUp()
{
openMocks(this);
given(auditModelRegistry.getAuditApplicationByName(APPLICATION_NAME)).willReturn(auditApplication);
given(auditApplication.getDisabledPathsId()).willReturn(DISABLED_PATHS_ID);
given(auditApplication.getApplicationKey()).willReturn(APP_KEY);
auditComponent.setDisabledPathsCacheEnabled(true);
mockedTxnSupport = mockStatic(AlfrescoTransactionSupport.class);
// Allow write-transaction check to pass without a real transaction context.
mockedTxnSupport.when(() -> AlfrescoTransactionSupport.checkTransactionReadState(true)).thenAnswer(inv -> null);
}
@After
public void tearDown()
{
mockedTxnSupport.close();
}
@Test
public void testGetDisabledPaths_cacheHit_doesNotCallDAO()
{
given(disabledPathsCache.get(DISABLED_PATHS_ID)).willReturn(new HashSet<>());
auditComponent.enableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should(never()).getPropertyById(any());
then(disabledPathsCache).should(never()).put(any(), any());
}
@Test
public void testGetDisabledPaths_cacheMiss_loadsFromDAOAndPopulatesCache()
{
Set<String> disabledPaths = new HashSet<>();
given(disabledPathsCache.get(DISABLED_PATHS_ID)).willReturn(null);
given(propertyValueDAO.getPropertyById(DISABLED_PATHS_ID)).willReturn((Serializable) disabledPaths);
auditComponent.enableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should().getPropertyById(DISABLED_PATHS_ID);
then(disabledPathsCache).should().put(eq(DISABLED_PATHS_ID), any());
}
@Test
public void testCacheInvalidated_afterEnableAudit_whenPathsChanged()
{
Set<String> disabledPaths = new HashSet<>();
disabledPaths.add("/" + APP_KEY);
given(disabledPathsCache.get(DISABLED_PATHS_ID)).willReturn(disabledPaths);
// null path resolves to "/" + APP_KEY, which is present in the disabled set.
auditComponent.enableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should().updateProperty(eq(DISABLED_PATHS_ID), any(Serializable.class));
then(disabledPathsCache).should().remove(DISABLED_PATHS_ID);
}
@Test
public void testCacheNotInvalidated_afterEnableAudit_whenNoPathsChanged()
{
given(disabledPathsCache.get(DISABLED_PATHS_ID)).willReturn(new HashSet<>());
auditComponent.enableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should(never()).updateProperty(any(), any(Serializable.class));
then(disabledPathsCache).should(never()).remove(any());
}
@Test
public void testCacheInvalidated_afterDisableAudit_whenPathAdded()
{
given(disabledPathsCache.get(DISABLED_PATHS_ID)).willReturn(new HashSet<>());
auditComponent.disableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should().updateProperty(eq(DISABLED_PATHS_ID), any(Serializable.class));
then(disabledPathsCache).should().remove(DISABLED_PATHS_ID);
}
@Test
public void testCacheNotInvalidated_afterDisableAudit_whenPathAlreadyDisabled()
{
Set<String> disabledPaths = new HashSet<>();
disabledPaths.add("/" + APP_KEY);
given(disabledPathsCache.get(DISABLED_PATHS_ID)).willReturn(disabledPaths);
// null path resolves to "/" + APP_KEY, which is already disabled — short-circuits.
auditComponent.disableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should(never()).updateProperty(any(), any(Serializable.class));
then(disabledPathsCache).should(never()).remove(any());
}
@Test
public void testCacheInvalidated_afterResetDisabledPaths()
{
auditComponent.resetDisabledPaths(APPLICATION_NAME);
then(propertyValueDAO).should().updateProperty(eq(DISABLED_PATHS_ID), any(Serializable.class));
then(disabledPathsCache).should().remove(DISABLED_PATHS_ID);
}
@Test
public void testGetDisabledPaths_cacheDisabled_alwaysCallsDAO()
{
auditComponent.setDisabledPathsCacheEnabled(false);
Set<String> disabledPaths = new HashSet<>();
given(propertyValueDAO.getPropertyById(DISABLED_PATHS_ID)).willReturn((Serializable) disabledPaths);
auditComponent.enableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should().getPropertyById(DISABLED_PATHS_ID);
then(disabledPathsCache).should(never()).get(any());
then(disabledPathsCache).should(never()).put(any(), any());
}
@Test
public void testCacheNotInvalidated_afterEnableAudit_whenCacheDisabled()
{
auditComponent.setDisabledPathsCacheEnabled(false);
Set<String> disabledPaths = new HashSet<>();
disabledPaths.add("/" + APP_KEY);
given(propertyValueDAO.getPropertyById(DISABLED_PATHS_ID)).willReturn((Serializable) disabledPaths);
auditComponent.enableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should().updateProperty(eq(DISABLED_PATHS_ID), any(Serializable.class));
then(disabledPathsCache).should(never()).remove(any());
}
@Test
public void testCacheNotInvalidated_afterDisableAudit_whenCacheDisabled()
{
auditComponent.setDisabledPathsCacheEnabled(false);
given(propertyValueDAO.getPropertyById(DISABLED_PATHS_ID)).willReturn((Serializable) new HashSet<>());
auditComponent.disableAudit(APPLICATION_NAME, null);
then(propertyValueDAO).should().updateProperty(eq(DISABLED_PATHS_ID), any(Serializable.class));
then(disabledPathsCache).should(never()).remove(any());
}
@Test
public void testCacheNotInvalidated_afterResetDisabledPaths_whenCacheDisabled()
{
auditComponent.setDisabledPathsCacheEnabled(false);
auditComponent.resetDisabledPaths(APPLICATION_NAME);
then(propertyValueDAO).should().updateProperty(eq(DISABLED_PATHS_ID), any(Serializable.class));
then(disabledPathsCache).should(never()).remove(any());
}
}