mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2026-09-16 18:13:17 +00:00
[MNT-25707] Audit disabled paths cache + Property value cache allowEqualsChecks configurable (#4062) (#4086)
This commit is contained in:
+3
-3
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user