From 9e23b990787d810e2987947d262b6a7e46cb2bcb Mon Sep 17 00:00:00 2001 From: Kacper Magdziarz <95610011+kmagdziarz@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:40:32 +0200 Subject: [PATCH] [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 --- .secrets.baseline | 6 +- pom.xml | 2 +- .../alfresco/repo/audit/AuditComponent.java | 7 + .../repo/audit/AuditComponentImpl.java | 87 +++++++----- .../org/alfresco/repo/audit/AuditRecord.java | 130 ++++++++++++++++++ .../repo/audit/AuditRecordReporter.java | 37 +++++ .../repo/audit/AuditRecordReporterImpl.java | 40 ++++++ .../alfresco/repo/audit/AuditRecordUtils.java | 104 ++++++++++++++ .../repo/audit/model/AuditModelRegistry.java | 15 ++ .../audit/model/AuditModelRegistryImpl.java | 25 ++++ .../alfresco/repo/event2/EventJSONSchema.java | 4 +- .../alfresco/audit-services-context.xml | 10 +- .../resources/alfresco/repository.properties | 2 + .../org/alfresco/AppContext02TestSuite.java | 1 + .../repo/audit/AuditComponentTest.java | 1 + .../repo/audit/AuditRecordUtilsTest.java | 123 +++++++++++++++++ .../alfresco/repo/audit/AuditTestSuite.java | 1 + 17 files changed, 552 insertions(+), 43 deletions(-) create mode 100644 repository/src/main/java/org/alfresco/repo/audit/AuditRecord.java create mode 100644 repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporter.java create mode 100644 repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporterImpl.java create mode 100644 repository/src/main/java/org/alfresco/repo/audit/AuditRecordUtils.java create mode 100644 repository/src/test/java/org/alfresco/repo/audit/AuditRecordUtilsTest.java diff --git a/.secrets.baseline b/.secrets.baseline index c2e187e741..8f15b752ef 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -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" } diff --git a/pom.xml b/pom.xml index eff065fc59..60b4e1e901 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 5.2.1 4.2.1 7.1 - 1.0.5 + 1.0.9 1.9.22.1 6.2.8 diff --git a/repository/src/main/java/org/alfresco/repo/audit/AuditComponent.java b/repository/src/main/java/org/alfresco/repo/audit/AuditComponent.java index 89730940d5..b000c2166d 100644 --- a/repository/src/main/java/org/alfresco/repo/audit/AuditComponent.java +++ b/repository/src/main/java/org/alfresco/repo/audit/AuditComponent.java @@ -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. * diff --git a/repository/src/main/java/org/alfresco/repo/audit/AuditComponentImpl.java b/repository/src/main/java/org/alfresco/repo/audit/AuditComponentImpl.java index 5ef5e12fa1..a45ce8587f 100644 --- a/repository/src/main/java/org/alfresco/repo/audit/AuditComponentImpl.java +++ b/repository/src/main/java/org/alfresco/repo/audit/AuditComponentImpl.java @@ -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 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 disabledPaths = (Set) propertyValueDAO.getPropertyById(disabledPathsId); - return new HashSet(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 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 pathedValues = new HashMap(values.size() * 2); + Map pathedValues = new HashMap<>(values.size() * 2); for (Map.Entry 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> callback = new RetryingTransactionCallback>() { - public Map execute() throws Throwable - { - return recordAuditValuesImpl(mappedValues); - } - }; + RetryingTransactionCallback> 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 recordAuditValuesImpl(Map mappedValues) { // Group the values by root path - Map> mappedValuesByRootKey = new HashMap>(); + Map> mappedValuesByRootKey = new HashMap<>(); for (Map.Entry entry : mappedValues.entrySet()) { String path = entry.getKey(); String rootKey = AuditApplication.getRootKey(path); - Map rootKeyMappedValues = mappedValuesByRootKey.get(rootKey); - if (rootKeyMappedValues == null) - { - rootKeyMappedValues = new HashMap(7); - mappedValuesByRootKey.put(rootKey, rootKeyMappedValues); - } + Map rootKeyMappedValues = mappedValuesByRootKey.computeIfAbsent(rootKey, k -> new HashMap<>(7)); rootKeyMappedValues.put(path, entry.getValue()); } - Map allAuditedValues = new HashMap(mappedValues.size() * 2 + 1); + Map allAuditedValues = new HashMap<>(mappedValues.size() * 2 + 1); // Now audit for each of the root keys for (Map.Entry> 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 auditData = generateData(generators); // Now extract values - Map extractedData = AuthenticationUtil.runAs(new RunAsWork>() { - public Map doWork() throws Exception - { - return extractData(application, values); - } - }, AuthenticationUtil.getSystemUserName()); + Map 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 argc = new HashMap(1); - argc.put(root.substring(index, root.length()).substring(1), value.getValue()); + Map 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 values) { - Map newData = new HashMap(values.size()); + Map newData = new HashMap<>(values.size()); List extractors = application.getDataExtractors(); for (DataExtractorDefinition extractorDef : extractors) @@ -900,7 +905,7 @@ public class AuditComponentImpl implements AuditComponent */ private Map generateData(Map generators) { - Map newData = new HashMap(generators.size() + 5); + Map newData = new HashMap<>(generators.size() + 5); for (Map.Entry 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 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} */ diff --git a/repository/src/main/java/org/alfresco/repo/audit/AuditRecord.java b/repository/src/main/java/org/alfresco/repo/audit/AuditRecord.java new file mode 100644 index 0000000000..2c6ab2d77f --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/audit/AuditRecord.java @@ -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 . + * #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 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 getAuditData() + { + return auditData; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String auditRecordType; + private boolean inTransaction; + private Map 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 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); + } + } + +} diff --git a/repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporter.java b/repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporter.java new file mode 100644 index 0000000000..7240b7193a --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporter.java @@ -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 . + * #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); +} diff --git a/repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporterImpl.java b/repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporterImpl.java new file mode 100644 index 0000000000..522d55d2e6 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/audit/AuditRecordReporterImpl.java @@ -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 . + * #L% + */ +package org.alfresco.repo.audit; + +public class AuditRecordReporterImpl implements AuditRecordReporter +{ + /** + * This method intentionally has an empty implementation. + *

+ * This class provides a no-op implementation of {@link AuditRecordReporter}. + */ + @Override + public void reportAuditRecord(AuditRecord auditRecord) + { + // No operation performed. + } +} diff --git a/repository/src/main/java/org/alfresco/repo/audit/AuditRecordUtils.java b/repository/src/main/java/org/alfresco/repo/audit/AuditRecordUtils.java new file mode 100644 index 0000000000..4b2ba81721 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/audit/AuditRecordUtils.java @@ -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 . + * #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. + *

+ * This method: + *

    + *
  • Translates flat {@code key-value} pairs into a nested JSON structure.
  • + *
  • Preloads the builder with the provided arguments.
  • + *
  • Splits keys by {@code /} to build the nested structure.
  • + *
  • Uses the root key as the application ID.
  • + *
  • Assumes each key starts with the same root, constructed as {@code '/' + auditedApplicationName + '/'}, which is removed before splitting.
  • + *
+ * + * @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 data, int keyRootLength) + { + var auditRecordBuilder = AuditRecord.builder(); + + var rootNode = createRootNode(data, keyRootLength); + + auditRecordBuilder.setAuditRecordData(rootNode); + + return auditRecordBuilder; + } + + @SuppressWarnings("unchecked") + private static HashMap createRootNode(Map data, int keyRootLength) + { + var rootNode = new HashMap(); + + data.forEach((k, v) -> { + var keys = k.substring(keyRootLength).split("/"); + + var current = rootNode; + for (int i = 0; i < keys.length - 1; i++) + { + current = (HashMap) current.computeIfAbsent(keys[i], newMap -> new HashMap()); + } + 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) value, 0); + } + else if (value instanceof NodeRef) + { + return ((NodeRef) value).getId(); + } + else + { + return value; + } + } +} diff --git a/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistry.java b/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistry.java index 9cb9c713df..e0d02e6d26 100644 --- a/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistry.java +++ b/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistry.java @@ -58,6 +58,21 @@ public interface AuditModelRegistry */ public boolean isAuditEnabled(); + /** + * Determines whether audit values should be stored in database. True by default if not changed by property. + * + * @return true if audit is enabled. + */ + boolean isAuditingToDatabaseEnabled(); + + /** + * Determines whether audit values should be stored in audit storage. + * + * @return true if auditing to Audit Storage is enabled. + * + */ + boolean isAuditingToAuditStorageEnabled(); + /** * Get a map of all audit applications key by name * diff --git a/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistryImpl.java b/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistryImpl.java index 0bf305ddd2..248ea8904e 100644 --- a/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistryImpl.java +++ b/repository/src/main/java/org/alfresco/repo/audit/model/AuditModelRegistryImpl.java @@ -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 diff --git a/repository/src/main/java/org/alfresco/repo/event2/EventJSONSchema.java b/repository/src/main/java/org/alfresco/repo/event2/EventJSONSchema.java index 49fe24c200..6bae6ecf76 100644 --- a/repository/src/main/java/org/alfresco/repo/event2/EventJSONSchema.java +++ b/repository/src/main/java/org/alfresco/repo/event2/EventJSONSchema.java @@ -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"; diff --git a/repository/src/main/resources/alfresco/audit-services-context.xml b/repository/src/main/resources/alfresco/audit-services-context.xml index 229703437a..0a80ea035a 100644 --- a/repository/src/main/resources/alfresco/audit-services-context.xml +++ b/repository/src/main/resources/alfresco/audit-services-context.xml @@ -37,8 +37,9 @@ + - + @@ -108,5 +109,8 @@ - - \ No newline at end of file + + + + + diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties index e9b55c5c3a..1c88e94740 100644 --- a/repository/src/main/resources/alfresco/repository.properties +++ b/repository/src/main/resources/alfresco/repository.properties @@ -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 diff --git a/repository/src/test/java/org/alfresco/AppContext02TestSuite.java b/repository/src/test/java/org/alfresco/AppContext02TestSuite.java index 5d6da7f4b3..7fb04e39f6 100644 --- a/repository/src/test/java/org/alfresco/AppContext02TestSuite.java +++ b/repository/src/test/java/org/alfresco/AppContext02TestSuite.java @@ -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, diff --git a/repository/src/test/java/org/alfresco/repo/audit/AuditComponentTest.java b/repository/src/test/java/org/alfresco/repo/audit/AuditComponentTest.java index b59ac8351c..76ce4a18a6 100644 --- a/repository/src/test/java/org/alfresco/repo/audit/AuditComponentTest.java +++ b/repository/src/test/java/org/alfresco/repo/audit/AuditComponentTest.java @@ -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"); diff --git a/repository/src/test/java/org/alfresco/repo/audit/AuditRecordUtilsTest.java b/repository/src/test/java/org/alfresco/repo/audit/AuditRecordUtilsTest.java new file mode 100644 index 0000000000..009c544686 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/audit/AuditRecordUtilsTest.java @@ -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 . + * #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(); + + 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) 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) 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(); + var expectedValue = new HashMap(); + + 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) auditData.get("CREATED"); + assertNotNull(created); + + assertEquals(1, created.size()); + var result = (HashMap) created.get("result"); + assertNotNull(result); + assertEquals(1, result.size()); + + var resultValue = (HashMap) 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")); + + } +} diff --git a/repository/src/test/java/org/alfresco/repo/audit/AuditTestSuite.java b/repository/src/test/java/org/alfresco/repo/audit/AuditTestSuite.java index 754f3412f8..b9c7c6bad8 100644 --- a/repository/src/test/java/org/alfresco/repo/audit/AuditTestSuite.java +++ b/repository/src/test/java/org/alfresco/repo/audit/AuditTestSuite.java @@ -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));