[ACS-9736] Add new bean for intercepting current audit entries. (#3402)

This introduces a new audit record reporting mechanism for intercepting current audit entries. The implementation adds the ability to report audit data to audit storage in addition to the traditional database storage.

Key changes:

- Added new audit record classes and utilities for structured audit data handling
- Introduced configurable audit destination settings (database vs audit storage)
- Created audit record reporter interface and implementation
This commit is contained in:
Kacper Magdziarz
2025-09-17 10:40:32 +02:00
committed by GitHub
parent ac36ac07e8
commit 9e23b99078
17 changed files with 552 additions and 43 deletions

View File

@@ -1242,7 +1242,7 @@
"filename": "repository/src/main/resources/alfresco/repository.properties",
"hashed_secret": "1459a56410378e4d3ab470eff570e5eae1742762",
"is_verified": false,
"line_number": 312,
"line_number": 314,
"is_secret": false
},
{
@@ -1250,7 +1250,7 @@
"filename": "repository/src/main/resources/alfresco/repository.properties",
"hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d",
"is_verified": false,
"line_number": 771,
"line_number": 773,
"is_secret": false
}
],
@@ -1845,5 +1845,5 @@
}
]
},
"generated_at": "2025-06-09T16:43:14Z"
"generated_at": "2025-07-23T08:25:11Z"
}

View File

@@ -54,7 +54,7 @@
<dependency.alfresco-transform-core.version>5.2.1</dependency.alfresco-transform-core.version>
<dependency.alfresco-transform-service.version>4.2.1</dependency.alfresco-transform-service.version>
<dependency.alfresco-greenmail.version>7.1</dependency.alfresco-greenmail.version>
<dependency.acs-event-model.version>1.0.5</dependency.acs-event-model.version>
<dependency.acs-event-model.version>1.0.9</dependency.acs-event-model.version>
<dependency.aspectj.version>1.9.22.1</dependency.aspectj.version>
<dependency.spring.version>6.2.8</dependency.spring.version>

View File

@@ -70,6 +70,13 @@ public interface AuditComponent
*/
public void setUserAuditFilter(UserAuditFilter userAuditFilter);
/**
* @param auditRecordReporter
* AuditRecordReporter
* @since 25.3
*/
public void setAuditRecordReporter(AuditRecordReporter auditRecordReporter);
/**
* Get all registered audit applications, whether active or not.
*

View File

@@ -48,7 +48,6 @@ import org.alfresco.repo.audit.model.AuditModelRegistryImpl;
import org.alfresco.repo.domain.audit.AuditDAO;
import org.alfresco.repo.domain.propval.PropertyValueDAO;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
@@ -73,8 +72,8 @@ public class AuditComponentImpl implements AuditComponent
{
private static final String INBOUND_LOGGER = "org.alfresco.repo.audit.inbound";
private static Log logger = LogFactory.getLog(AuditComponentImpl.class);
private static Log loggerInbound = LogFactory.getLog(INBOUND_LOGGER);
private static final Log logger = LogFactory.getLog(AuditComponentImpl.class);
private static final Log loggerInbound = LogFactory.getLog(INBOUND_LOGGER);
private AuditModelRegistryImpl auditModelRegistry;
private PropertyValueDAO propertyValueDAO;
@@ -82,6 +81,7 @@ public class AuditComponentImpl implements AuditComponent
private TransactionService transactionService;
private AuditFilter auditFilter;
private UserAuditFilter userAuditFilter;
private AuditRecordReporter auditRecordReporter;
/**
* Default constructor
@@ -140,6 +140,11 @@ public class AuditComponentImpl implements AuditComponent
this.userAuditFilter = userAuditFilter;
}
public void setAuditRecordReporter(AuditRecordReporter auditRecordReporter)
{
this.auditRecordReporter = auditRecordReporter;
}
/**
* {@inheritDoc}
*
@@ -215,7 +220,7 @@ public class AuditComponentImpl implements AuditComponent
public int deleteAuditEntries(List<Long> auditEntryIds)
{
// Shortcut, if necessary
if (auditEntryIds.size() == 0)
if (auditEntryIds.isEmpty())
{
return 0;
}
@@ -234,7 +239,7 @@ public class AuditComponentImpl implements AuditComponent
{
Long disabledPathsId = application.getDisabledPathsId();
Set<String> disabledPaths = (Set<String>) propertyValueDAO.getPropertyById(disabledPathsId);
return new HashSet<String>(disabledPaths);
return new HashSet<>(disabledPaths);
}
catch (Throwable e)
{
@@ -254,6 +259,16 @@ public class AuditComponentImpl implements AuditComponent
return auditModelRegistry.isAuditEnabled();
}
public boolean isAuditingToDatabaseEnabled()
{
return auditModelRegistry.isAuditingToDatabaseEnabled();
}
public boolean isAuditingToAuditStorageEnabled()
{
return auditModelRegistry.isAuditingToAuditStorageEnabled();
}
/**
* {@inheritDoc}
*
@@ -309,7 +324,7 @@ public class AuditComponentImpl implements AuditComponent
{
PathMapper pathMapper = auditModelRegistry.getAuditPathMapper();
Set<String> mappedPaths = pathMapper.getMappedPathsWithPartialMatch(path);
return loggerInbound.isDebugEnabled() || mappedPaths.size() > 0;
return loggerInbound.isDebugEnabled() || !mappedPaths.isEmpty();
}
/**
@@ -346,7 +361,7 @@ public class AuditComponentImpl implements AuditComponent
// Check if there are any entries that match or supercede the given path
String disablingPath = null;
;
for (String disabledPath : disabledPaths)
{
if (path.startsWith(disabledPath))
@@ -573,7 +588,7 @@ public class AuditComponentImpl implements AuditComponent
}
// Build the key paths using the session root path
Map<String, Serializable> pathedValues = new HashMap<String, Serializable>(values.size() * 2);
Map<String, Serializable> pathedValues = new HashMap<>(values.size() * 2);
for (Map.Entry<String, Serializable> entry : values.entrySet())
{
String pathElement = entry.getKey();
@@ -596,12 +611,7 @@ public class AuditComponentImpl implements AuditComponent
case TXN_NONE:
case TXN_READ_ONLY:
// New transaction
RetryingTransactionCallback<Map<String, Serializable>> callback = new RetryingTransactionCallback<Map<String, Serializable>>() {
public Map<String, Serializable> execute() throws Throwable
{
return recordAuditValuesImpl(mappedValues);
}
};
RetryingTransactionCallback<Map<String, Serializable>> callback = () -> recordAuditValuesImpl(mappedValues);
RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
txnHelper.setForceWritable(true);
return txnHelper.doInTransaction(callback, false, true);
@@ -618,21 +628,16 @@ public class AuditComponentImpl implements AuditComponent
public Map<String, Serializable> recordAuditValuesImpl(Map<String, Serializable> mappedValues)
{
// Group the values by root path
Map<String, Map<String, Serializable>> mappedValuesByRootKey = new HashMap<String, Map<String, Serializable>>();
Map<String, Map<String, Serializable>> mappedValuesByRootKey = new HashMap<>();
for (Map.Entry<String, Serializable> entry : mappedValues.entrySet())
{
String path = entry.getKey();
String rootKey = AuditApplication.getRootKey(path);
Map<String, Serializable> rootKeyMappedValues = mappedValuesByRootKey.get(rootKey);
if (rootKeyMappedValues == null)
{
rootKeyMappedValues = new HashMap<String, Serializable>(7);
mappedValuesByRootKey.put(rootKey, rootKeyMappedValues);
}
Map<String, Serializable> rootKeyMappedValues = mappedValuesByRootKey.computeIfAbsent(rootKey, k -> new HashMap<>(7));
rootKeyMappedValues.put(path, entry.getValue());
}
Map<String, Serializable> allAuditedValues = new HashMap<String, Serializable>(mappedValues.size() * 2 + 1);
Map<String, Serializable> allAuditedValues = new HashMap<>(mappedValues.size() * 2 + 1);
// Now audit for each of the root keys
for (Map.Entry<String, Map<String, Serializable>> entry : mappedValuesByRootKey.entrySet())
{
@@ -694,7 +699,7 @@ public class AuditComponentImpl implements AuditComponent
}
// Check if there is anything to audit
if (values.size() == 0)
if (values.isEmpty())
{
if (logger.isDebugEnabled())
{
@@ -727,12 +732,7 @@ public class AuditComponentImpl implements AuditComponent
Map<String, Serializable> auditData = generateData(generators);
// Now extract values
Map<String, Serializable> extractedData = AuthenticationUtil.runAs(new RunAsWork<Map<String, Serializable>>() {
public Map<String, Serializable> doWork() throws Exception
{
return extractData(application, values);
}
}, AuthenticationUtil.getSystemUserName());
Map<String, Serializable> extractedData = AuthenticationUtil.runAs(() -> extractData(application, values), AuthenticationUtil.getSystemUserName());
// Combine extracted and generated values (extracted data takes precedence)
auditData.putAll(extractedData);
@@ -743,8 +743,8 @@ public class AuditComponentImpl implements AuditComponent
{
String root = value.getKey();
int index = root.lastIndexOf("/");
Map<String, Serializable> argc = new HashMap<String, Serializable>(1);
argc.put(root.substring(index, root.length()).substring(1), value.getValue());
Map<String, Serializable> argc = new HashMap<>(1);
argc.put(root.substring(index).substring(1), value.getValue());
if (!auditFilter.accept(root.substring(0, index), argc))
{
return Collections.emptyMap();
@@ -760,10 +760,15 @@ public class AuditComponentImpl implements AuditComponent
{
// Persist the values (if not just gathering data in a pre call for use in a post call)
boolean justGatherPreCallData = application.isApplicationJustGeneratingPreCallData();
if (!justGatherPreCallData)
if (!justGatherPreCallData && isAuditingToDatabaseEnabled())
{
entryId = auditDAO.createAuditEntry(applicationId, time, username, auditData);
}
if (isAuditingToAuditStorageEnabled())
{
auditRecordReporter.reportAuditRecord(createAuditRecord(auditData, true, username, entryId, application.getApplicationName()));
}
// Done
if (logger.isDebugEnabled())
{
@@ -822,7 +827,7 @@ public class AuditComponentImpl implements AuditComponent
AuditApplication application,
Map<String, Serializable> values)
{
Map<String, Serializable> newData = new HashMap<String, Serializable>(values.size());
Map<String, Serializable> newData = new HashMap<>(values.size());
List<DataExtractorDefinition> extractors = application.getDataExtractors();
for (DataExtractorDefinition extractorDef : extractors)
@@ -900,7 +905,7 @@ public class AuditComponentImpl implements AuditComponent
*/
private Map<String, Serializable> generateData(Map<String, DataGenerator> generators)
{
Map<String, Serializable> newData = new HashMap<String, Serializable>(generators.size() + 5);
Map<String, Serializable> newData = new HashMap<>(generators.size() + 5);
for (Map.Entry<String, DataGenerator> entry : generators.entrySet())
{
String path = entry.getKey();
@@ -925,6 +930,20 @@ public class AuditComponentImpl implements AuditComponent
return newData;
}
/**
* Creates an AuditRecord from the provided audit data.
*/
private AuditRecord createAuditRecord(Map<String, Serializable> auditData, boolean inTransaction, String username, Long entryId, String applicationName)
{
int rootSize = applicationName.length() + 2; // Root is constructed like this -> '/' + auditedApplicationName + '/'.
AuditRecord.Builder builder = AuditRecordUtils.generateAuditRecordBuilder(auditData, rootSize);
builder.setAuditRecordType(applicationName);
builder.setInTransaction(inTransaction);
builder.setUsername(username);
builder.setEntryDBId(entryId);
return builder.build();
}
/**
* {@inheritDoc}
*/

View File

@@ -0,0 +1,130 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2025 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 java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.Map;
public class AuditRecord
{
private final boolean inTransaction;
private final String auditApplicationId;
private final ZonedDateTime createdAt;
private final String username;
private final Long entryDBId;
private final Map<String, Serializable> auditData;
public AuditRecord(Builder builder)
{
this.auditApplicationId = builder.auditRecordType;
this.inTransaction = builder.inTransaction;
this.auditData = builder.auditRecordData;
this.createdAt = ZonedDateTime.now();
this.username = builder.username;
this.entryDBId = builder.entryDBId;
}
public String getAuditApplicationId()
{
return auditApplicationId;
}
public boolean isInTransaction()
{
return inTransaction;
}
public ZonedDateTime getCreatedAt()
{
return createdAt;
}
public String getUsername()
{
return username;
}
public Long getEntryDBId()
{
return entryDBId;
}
public Map<String, Serializable> getAuditData()
{
return auditData;
}
public static Builder builder()
{
return new Builder();
}
public static class Builder
{
private String auditRecordType;
private boolean inTransaction;
private Map<String, Serializable> auditRecordData;
private String username;
private Long entryDBId;
public Builder setAuditRecordType(String auditRecordType)
{
this.auditRecordType = auditRecordType;
return this;
}
public Builder setInTransaction(boolean inTransaction)
{
this.inTransaction = inTransaction;
return this;
}
public Builder setAuditRecordData(Map<String, Serializable> auditRecordData)
{
this.auditRecordData = auditRecordData;
return this;
}
public Builder setUsername(String username)
{
this.username = username;
return this;
}
public Builder setEntryDBId(Long entryDBId)
{
this.entryDBId = entryDBId;
return this;
}
public AuditRecord build()
{
return new AuditRecord(this);
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2025 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;
public interface AuditRecordReporter
{
/**
* This method will report AuditRecord to Audit Storage using RepoEvent2
*
* @param auditRecord
* represent data that will be reported.
*/
void reportAuditRecord(AuditRecord auditRecord);
}

View File

@@ -0,0 +1,40 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2025 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;
public class AuditRecordReporterImpl implements AuditRecordReporter
{
/**
* This method intentionally has an empty implementation.
* <p>
* This class provides a no-op implementation of {@link AuditRecordReporter}.
*/
@Override
public void reportAuditRecord(AuditRecord auditRecord)
{
// No operation performed.
}
}

View File

@@ -0,0 +1,104 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2025 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 java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.service.cmr.repository.NodeRef;
public final class AuditRecordUtils
{
private AuditRecordUtils()
{
// This is a utility class and cannot be instantiated.
}
/**
* Generates an {@link AuditRecord.Builder} from flat audit data.
* <p>
* This method:
* <ul>
* <li>Translates flat {@code key-value} pairs into a nested JSON structure.</li>
* <li>Preloads the builder with the provided arguments.</li>
* <li>Splits keys by {@code /} to build the nested structure.</li>
* <li>Uses the root key as the application ID.</li>
* <li>Assumes each key starts with the same root, constructed as {@code '/' + auditedApplicationName + '/'}, which is removed before splitting.</li>
* </ul>
*
* @param data
* a map containing flat audit data as `key-value` pairs
* @param keyRootLength
* is a length of key root.
* @return a preloaded {@link AuditRecord.Builder}
*/
public static AuditRecord.Builder generateAuditRecordBuilder(Map<String, Serializable> data, int keyRootLength)
{
var auditRecordBuilder = AuditRecord.builder();
var rootNode = createRootNode(data, keyRootLength);
auditRecordBuilder.setAuditRecordData(rootNode);
return auditRecordBuilder;
}
@SuppressWarnings("unchecked")
private static HashMap<String, Serializable> createRootNode(Map<String, Serializable> data, int keyRootLength)
{
var rootNode = new HashMap<String, Serializable>();
data.forEach((k, v) -> {
var keys = k.substring(keyRootLength).split("/");
var current = rootNode;
for (int i = 0; i < keys.length - 1; i++)
{
current = (HashMap<String, Serializable>) current.computeIfAbsent(keys[i], newMap -> new HashMap<String, Serializable>());
}
current.put(keys[keys.length - 1], decodeValueByInstance(v));
});
return rootNode;
}
@SuppressWarnings("unchecked")
private static Serializable decodeValueByInstance(Serializable value)
{
if (value instanceof HashMap<?, ?>)
{
return createRootNode((HashMap<String, Serializable>) value, 0);
}
else if (value instanceof NodeRef)
{
return ((NodeRef) value).getId();
}
else
{
return value;
}
}
}

View File

@@ -58,6 +58,21 @@ public interface AuditModelRegistry
*/
public boolean isAuditEnabled();
/**
* Determines whether audit values should be stored in database. <code>True</code> by default if not changed by property.
*
* @return <code>true</code> if audit is enabled.
*/
boolean isAuditingToDatabaseEnabled();
/**
* Determines whether audit values should be stored in audit storage.
*
* @return <code>true</code> if auditing to Audit Storage is enabled.
*
*/
boolean isAuditingToAuditStorageEnabled();
/**
* Get a map of all audit applications key by name
*

View File

@@ -85,6 +85,9 @@ public class AuditModelRegistryImpl extends AbstractPropertyBackedBean implement
{
/** The name of the global enablement property. */
public static final String PROPERTY_AUDIT_ENABLED = "audit.enabled";
private static final String AUDITING_TO_DATABASE = ".auditingToDatabase";
private static final String AUDITING_TO_AUDIT_STORAGE = ".auditingToAuditStorage";
/** The name of the strict loading flag. */
public static final String PROPERTY_AUDIT_CONFIG_STRICT = "audit.config.strict";
/** The XSD classpath location. */
@@ -249,6 +252,26 @@ public class AuditModelRegistryImpl extends AbstractPropertyBackedBean implement
return value != null && value.equalsIgnoreCase("true");
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAuditingToDatabaseEnabled()
{
String value = getProperty(AUDIT_PROPERTY_AUDIT_ENABLED + AUDITING_TO_DATABASE);
return value == null || value.equalsIgnoreCase("true");
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAuditingToAuditStorageEnabled()
{
String value = getProperty(AUDIT_PROPERTY_AUDIT_ENABLED + AUDITING_TO_AUDIT_STORAGE);
return value != null && value.equalsIgnoreCase("true");
}
/**
* Enables audit and registers an audit model at a given URL. Does not register across the cluster and should only be used for unit test purposes.
*
@@ -296,6 +319,8 @@ public class AuditModelRegistryImpl extends AbstractPropertyBackedBean implement
// Default value for global enabled property
properties.put(AUDIT_PROPERTY_AUDIT_ENABLED, false);
properties.put(AUDIT_PROPERTY_AUDIT_ENABLED + AUDITING_TO_DATABASE, true);
properties.put(AUDIT_PROPERTY_AUDIT_ENABLED + AUDITING_TO_AUDIT_STORAGE, false);
// Let's search for config files in the appropriate places. The individual applications they contain can still
// be enabled/disabled by the bean properties

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -38,7 +38,7 @@ import org.alfresco.util.Pair;
*/
public enum EventJSONSchema
{
NODE_CREATED_V1("nodeCreated", 1, EventType.NODE_CREATED), NODE_UPDATED_V1("nodeUpdated", 1, EventType.NODE_UPDATED), NODE_DELETED_V1("nodeDeleted", 1, EventType.NODE_DELETED), CHILD_ASSOC_CREATED_V1("childAssocCreated", 1, EventType.CHILD_ASSOC_CREATED), CHILD_ASSOC_DELETED_V1("childAssocDeleted", 1, EventType.CHILD_ASSOC_DELETED), PEER_ASSOC_CREATED_V1("peerAssocCreated", 1, EventType.PEER_ASSOC_CREATED), PEER_ASSOC_DELETED_V1("peerAssocDeleted", 1, EventType.PEER_ASSOC_DELETED), PERMISSION_UPDATED_V1("permissionUpdated", 1, EventType.PERMISSION_UPDATED);
NODE_CREATED_V1("nodeCreated", 1, EventType.NODE_CREATED), NODE_UPDATED_V1("nodeUpdated", 1, EventType.NODE_UPDATED), NODE_DELETED_V1("nodeDeleted", 1, EventType.NODE_DELETED), CHILD_ASSOC_CREATED_V1("childAssocCreated", 1, EventType.CHILD_ASSOC_CREATED), CHILD_ASSOC_DELETED_V1("childAssocDeleted", 1, EventType.CHILD_ASSOC_DELETED), PEER_ASSOC_CREATED_V1("peerAssocCreated", 1, EventType.PEER_ASSOC_CREATED), PEER_ASSOC_DELETED_V1("peerAssocDeleted", 1, EventType.PEER_ASSOC_DELETED), PERMISSION_UPDATED_V1("permissionUpdated", 1, EventType.PERMISSION_UPDATED), AUDIT_ENTRY_CREATED_V1("auditEntryCreated", 1, EventType.AUDIT_ENTRY_CREATED);
private static final String PREFIX = "https://api.alfresco.com/schema/event/repo/v";

View File

@@ -37,8 +37,9 @@
<property name="properties" ref="global-properties" />
</bean>
</property>
<property name="auditRecordReporter" ref="auditRecordReporter"/>
</bean>
<!-- User Audit Filter -->
<bean id="userAuditFilter" class="org.alfresco.repo.audit.UserAuditFilter">
@@ -108,5 +109,8 @@
<!-- Reference in the audit registry managed bean -->
<alias name="Audit" alias="auditModel.modelRegistry"/>
</beans>
<!-- Audit Record Reported -->
<bean id="auditRecordReporter" class="org.alfresco.repo.audit.AuditRecordReporterImpl"/>
</beans>

View File

@@ -286,6 +286,8 @@ audit.alfresco-access.enabled=false
audit.alfresco-access.sub-actions.enabled=false
audit.cmischangelog.enabled=false
audit.dod5015.enabled=false
audit.enabled.auditingToAuditStorage=false
audit.enabled.auditingToDatabase=true
# Setting this flag to true will force startup failure when invalid audit configurations are detected
audit.config.strict=false
# Audit map filter for AccessAuditor - restricts recorded events to user driven events

View File

@@ -48,6 +48,7 @@ import org.alfresco.util.testing.category.NonBuildTests;
org.alfresco.repo.audit.UserAuditFilterTest.class,
org.alfresco.repo.audit.AuditMethodInterceptorTest.class,
org.alfresco.repo.audit.access.AccessAuditorTest.class,
org.alfresco.repo.audit.AuditRecordUtilsTest.class,
// the following test will lock up the DB if run in the applicationContext_01 test suite
org.alfresco.repo.activities.feed.FeedNotifierTest.class,

View File

@@ -877,6 +877,7 @@ public class AuditComponentTest extends TestCase
auditModelRegistry.loadAuditModels();
auditModelRegistry.setProperty("audit.enabled", "true");
auditModelRegistry.setProperty("audit.enabled.auditingToDatabase", "true");
auditModelRegistry.setProperty("audit.app1.enabled", "true");
auditModelRegistry.setProperty("audit.filter.app1.default.enabled", "true");

View File

@@ -0,0 +1,123 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2025 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.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.Serializable;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
public class AuditRecordUtilsTest
{
@SuppressWarnings("unchecked")
@Test
public void testGenerateAuditRecordBuilderTest()
{
var testData = new HashMap<String, Serializable>();
testData.put("/alfresco-access/transaction/path", "/app:company_home");
testData.put("/alfresco-access/transaction/user", "admin");
testData.put("/alfresco-access/transaction/sub-actions", "updateNodeProperties");
var now = Instant.now();
testData.put("/alfresco-access/transaction/properties/from", (Serializable) Map.of(QName.createQName("modified"), Date.from(now)));
testData.put("/alfresco-access/transaction/properties/to", (Serializable) Map.of(QName.createQName("modified"), Date.from(now)));
var builder = AuditRecordUtils.generateAuditRecordBuilder(testData, "/alfresco-access/".length());
builder.setAuditRecordType("alfresco-access");
var auditRecord = builder.build();
assertNotNull(auditRecord);
assertEquals("alfresco-access", auditRecord.getAuditApplicationId());
var auditData = auditRecord.getAuditData();
assertEquals(1, auditData.size());
var transaction = (HashMap<String, ?>) auditData.get("transaction");
assertNotNull(transaction);
assertEquals(4, transaction.size());
assertEquals(testData.get("/alfresco-access/transaction/path"), transaction.get("path"));
assertEquals(testData.get("/alfresco-access/transaction/user"), transaction.get("user"));
assertEquals(testData.get("/alfresco-access/transaction/sub-actions"), transaction.get("sub-actions"));
var properties = (HashMap<String, Object>) transaction.get("properties");
assertNotNull(properties);
assertEquals(2, properties.size());
assertEquals(testData.get("/alfresco-access/transaction/properties/from"), properties.get("from"));
assertEquals(testData.get("/alfresco-access/transaction/properties/to"), properties.get("to"));
}
@SuppressWarnings("unchecked")
@Test
public void testGenerateAuditRecordBuilderTestNodeRef()
{
var testData = new HashMap<String, Serializable>();
var expectedValue = new HashMap<String, Serializable>();
expectedValue.put("nodeRef", new NodeRef("workspace://SpacesStore/bfa612e6-1a02-46a0-a612-e61a02e6a036"));
expectedValue.put("objectId", "bfa612e6-1a02-46a0-a612-e61a02e6a036;1.0");
testData.put("/CMISChangeLog/CREATED/result/value", expectedValue);
var builder = AuditRecordUtils.generateAuditRecordBuilder(testData, "/CMISChangeLog/".length());
builder.setAuditRecordType("CMISChangeLog");
var auditRecord = builder.build();
assertNotNull(auditRecord);
assertEquals("CMISChangeLog", auditRecord.getAuditApplicationId());
var auditData = auditRecord.getAuditData();
assertEquals(1, auditData.size());
var created = (HashMap<String, ?>) auditData.get("CREATED");
assertNotNull(created);
assertEquals(1, created.size());
var result = (HashMap<String, Object>) created.get("result");
assertNotNull(result);
assertEquals(1, result.size());
var resultValue = (HashMap<String, Object>) result.get("value");
assertNotNull(resultValue);
assertEquals(2, resultValue.size());
var expectedNodeRef = (NodeRef) expectedValue.get("nodeRef");
assertEquals(expectedNodeRef.getId(), resultValue.get("nodeRef"));
assertEquals(expectedValue.get("objectId"), resultValue.get("objectId"));
}
}

View File

@@ -50,6 +50,7 @@ public class AuditTestSuite extends TestSuite
suite.addTestSuite(UserAuditFilterTest.class);
suite.addTestSuite(AuditMethodInterceptorTest.class);
suite.addTest(new JUnit4TestAdapter(AuditRecordUtilsTest.class));
suite.addTest(new JUnit4TestAdapter(PropertyAuditFilterTest.class));
suite.addTest(new JUnit4TestAdapter(AccessAuditorTest.class));