/* * Copyright (C) 2005-2014 Alfresco Software Limited. * * This file is part of Alfresco * * 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 . */ package org.alfresco.repo.audit; import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.audit.extractor.DataExtractor; import org.alfresco.repo.audit.generator.DataGenerator; import org.alfresco.repo.audit.model.AuditApplication; import org.alfresco.repo.audit.model.AuditModelRegistry; import org.alfresco.repo.audit.model.AuditModelRegistryImpl; import org.alfresco.repo.audit.model.AuditApplication.DataExtractorDefinition; import org.alfresco.repo.domain.audit.AuditDAO; import org.alfresco.repo.domain.propval.PropertyValueDAO; import org.alfresco.repo.domain.schema.SchemaBootstrap; 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; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.audit.AuditQueryParameters; import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; import org.alfresco.service.cmr.repository.MLText; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.PathMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.extensions.surf.util.ParameterCheck; /** * Component that records audit values as well as providing the query implementation. *

* To turn on logging of all potentially auditable data, turn on logging for:
* {@link #INBOUND_LOGGER org.alfresco.repo.audit.inbound}. *

* TODO: Respect audit internal - at the moment audit internal is fixed to false. * * @author Derek Hulley * @since 3.2 (in its current form) */ 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 AuditModelRegistryImpl auditModelRegistry; private PropertyValueDAO propertyValueDAO; private AuditDAO auditDAO; private TransactionService transactionService; private AuditFilter auditFilter; private UserAuditFilter userAuditFilter; /** * Default constructor */ public AuditComponentImpl() { } /** * Set the registry holding the audit models * @since 3.2 */ public void setAuditModelRegistry(AuditModelRegistryImpl auditModelRegistry) { this.auditModelRegistry = auditModelRegistry; } /** * Set the DAO for manipulating property values * @since 3.2 */ public void setPropertyValueDAO(PropertyValueDAO propertyValueDAO) { this.propertyValueDAO = propertyValueDAO; } /** * Set the DAO for accessing audit data * @since 3.2 */ public void setAuditDAO(AuditDAO auditDAO) { this.auditDAO = auditDAO; } /** * Set the service used to start new transactions */ public void setTransactionService(TransactionService transactionService) { this.transactionService = transactionService; } /** * Set the component used to filter which audit events to record */ public void setAuditFilter(AuditFilter auditFilter) { this.auditFilter = auditFilter; } public void setUserAuditFilter(UserAuditFilter userAuditFilter) { this.userAuditFilter = userAuditFilter; } /** * {@inheritDoc} * @since 3.2 */ public int deleteAuditEntries(String applicationName, Long fromTime, Long toTime) { ParameterCheck.mandatory("applicationName", applicationName); AlfrescoTransactionSupport.checkTransactionReadState(true); AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName); if (application == null) { if (logger.isDebugEnabled()) { logger.debug("No audit application named '" + applicationName + "' has been registered."); } return 0; } Long applicationId = application.getApplicationId(); int deleted = auditDAO.deleteAuditEntries(applicationId, fromTime, toTime); // Done if (logger.isDebugEnabled()) { logger.debug( "Delete audit " + deleted + " entries for " + applicationName + " (" + fromTime + " to " + toTime); } return deleted; } /** * {@inheritDoc} * @since 3.2 */ @Override public int deleteAuditEntries(List auditEntryIds) { // Shortcut, if necessary if (auditEntryIds.size() == 0) { return 0; } return auditDAO.deleteAuditEntries(auditEntryIds); } /** * @param application the audit application object * @return Returns a copy of the set of disabled paths associated with the application */ @SuppressWarnings("unchecked") private Set getDisabledPaths(AuditApplication application) { try { Long disabledPathsId = application.getDisabledPathsId(); Set disabledPaths = (Set) propertyValueDAO.getPropertyById(disabledPathsId); return new HashSet(disabledPaths); } catch (Throwable e) { // Might be an invalid ID, somehow auditModelRegistry.loadAuditModels(); throw new AlfrescoRuntimeException("Unabled to get AuditApplication disabled paths: " + application, e); } } /** * {@inheritDoc} * @since 3.2 */ public boolean isAuditEnabled() { return auditModelRegistry.isAuditEnabled(); } /** * {@inheritDoc} * @since 3.4 */ @Override public void setAuditEnabled(boolean enable) { boolean alreadyEnabled = auditModelRegistry.isAuditEnabled(); if (alreadyEnabled != enable) { // It is changing auditModelRegistry.stop(); auditModelRegistry.setProperty( AuditModelRegistry.AUDIT_PROPERTY_AUDIT_ENABLED, Boolean.toString(enable).toLowerCase()); auditModelRegistry.start(); } } /** * {@inheritDoc} * @since 3.4 */ public Map getAuditApplications() { return auditModelRegistry.getAuditApplications(); } /** * {@inheritDoc} *

* Note that if DEBUG is on for the the {@link #INBOUND_LOGGER}, then true * will always be returned. * * @since 3.2 */ public boolean areAuditValuesRequired() { return (loggerInbound.isDebugEnabled()) || (isAuditEnabled() && !auditModelRegistry.getAuditPathMapper().isEmpty()); } /** * {@inheritDoc} * @since 3.4 */ @Override public boolean areAuditValuesRequired(String path) { PathMapper pathMapper = auditModelRegistry.getAuditPathMapper(); Set mappedPaths = pathMapper.getMappedPathsWithPartialMatch(path); return mappedPaths.size() > 0; } /** * {@inheritDoc} * @since 3.2 */ public boolean isAuditPathEnabled(String applicationName, String path) { ParameterCheck.mandatory("applicationName", applicationName); AlfrescoTransactionSupport.checkTransactionReadState(false); AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName); if (application == null) { if (logger.isDebugEnabled()) { logger.debug("No audit application named '" + applicationName + "' has been registered."); } return false; } // Ensure that the path gets a valid value if (path == null) { path = AuditApplication.AUDIT_PATH_SEPARATOR + application.getApplicationKey(); } else { // Check the path against the application application.checkPath(path); } Set disabledPaths = getDisabledPaths(application); // Check if there are any entries that match or supercede the given path String disablingPath = null;; for (String disabledPath : disabledPaths) { if (path.startsWith(disabledPath)) { disablingPath = disabledPath; break; } } // Done if (logger.isDebugEnabled()) { logger.debug( "Audit path enabled check: \n" + " Application: " + applicationName + "\n" + " Path: " + path + "\n" + " Disabling Path: " + disablingPath); } return disablingPath == null; } /** * {@inheritDoc} * @since 3.2 */ public void enableAudit(String applicationName, String path) { ParameterCheck.mandatory("applicationName", applicationName); AlfrescoTransactionSupport.checkTransactionReadState(true); AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName); if (application == null) { if (logger.isDebugEnabled()) { logger.debug("No audit application named '" + applicationName + "' has been registered."); } return; } // Ensure that the path gets a valid value if (path == null) { path = AuditApplication.AUDIT_PATH_SEPARATOR + application.getApplicationKey(); } else { // Check the path against the application application.checkPath(path); } Long disabledPathsId = application.getDisabledPathsId(); Set disabledPaths = getDisabledPaths(application); // Remove any paths that start with the given path boolean changed = false; Iterator iterateDisabledPaths = disabledPaths.iterator(); while (iterateDisabledPaths.hasNext()) { String disabledPath = iterateDisabledPaths.next(); if (disabledPath.startsWith(path)) { iterateDisabledPaths.remove(); changed = true; } } // Persist, if necessary if (changed) { propertyValueDAO.updateProperty(disabledPathsId, (Serializable) disabledPaths); if (logger.isDebugEnabled()) { logger.debug( "Audit disabled paths updated: \n" + " Application: " + applicationName + "\n" + " Disabled: " + disabledPaths); } } // Done } /** * {@inheritDoc} * @since 3.2 */ public void disableAudit(String applicationName, String path) { ParameterCheck.mandatory("applicationName", applicationName); AlfrescoTransactionSupport.checkTransactionReadState(true); AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName); if (application == null) { if (logger.isDebugEnabled()) { logger.debug("No audit application named '" + applicationName + "' has been registered."); } return; } // Ensure that the path gets a valid value if (path == null) { path = AuditApplication.AUDIT_PATH_SEPARATOR + application.getApplicationKey(); } else { // Check the path against the application application.checkPath(path); } Long disabledPathsId = application.getDisabledPathsId(); Set disabledPaths = getDisabledPaths(application); // Shortcut if the disabled paths contain the exact path if (disabledPaths.contains(path)) { if (logger.isDebugEnabled()) { logger.debug( "Audit disable path already present: \n" + " Path: " + path); } return; } // Bring the set up to date by stripping out unwanted paths Iterator iterateDisabledPaths = disabledPaths.iterator(); while (iterateDisabledPaths.hasNext()) { String disabledPath = iterateDisabledPaths.next(); if (disabledPath.startsWith(path)) { // We will be superceding this iterateDisabledPaths.remove(); } else if (path.startsWith(disabledPath)) { // There is already a superceding path if (logger.isDebugEnabled()) { logger.debug( "Audit disable path superceded: \n" + " Path: " + path + "\n" + " Superceded by: " + disabledPath); } return; } } // Add our path in disabledPaths.add(path); // Upload the new set propertyValueDAO.updateProperty(disabledPathsId, (Serializable) disabledPaths); // Done if (logger.isDebugEnabled()) { logger.debug( "Audit disabled paths updated: \n" + " Application: " + applicationName + "\n" + " Disabled: " + disabledPaths); } } /** * {@inheritDoc} * @since 3.2 */ public void resetDisabledPaths(String applicationName) { ParameterCheck.mandatory("applicationName", applicationName); AlfrescoTransactionSupport.checkTransactionReadState(true); AuditApplication application = auditModelRegistry.getAuditApplicationByName(applicationName); if (application == null) { if (logger.isDebugEnabled()) { logger.debug("No audit application named '" + applicationName + "' has been registered."); } return; } Long disabledPathsId = application.getDisabledPathsId(); propertyValueDAO.updateProperty(disabledPathsId, (Serializable) Collections.emptySet()); // Done if (logger.isDebugEnabled()) { logger.debug("Removed all disabled paths for application " + applicationName); } } /** * {@inheritDoc} * @since 3.2 */ public Map recordAuditValues(String rootPath, Map values) { ParameterCheck.mandatory("rootPath", rootPath); AuditApplication.checkPathFormat(rootPath); String username = AuthenticationUtil.getFullyAuthenticatedUser(); if (values == null || values.isEmpty() || !areAuditValuesRequired() || !userAuditFilter.acceptUser(username) || !auditFilter.accept(rootPath, values)) { return Collections.emptyMap(); } // trim string audited value for (Map.Entry entry : values.entrySet()) { Serializable auditValue = entry.getValue(); // Trim strings if (auditValue instanceof String) { entry.setValue(SchemaBootstrap.trimStringForTextFields((String) auditValue)); } else if (auditValue instanceof MLText) { MLText mltext = (MLText) auditValue; Set locales = mltext.getLocales(); for (Locale locale : locales) { mltext.put(locale, SchemaBootstrap.trimStringForTextFields(mltext.getValue(locale))); } entry.setValue(mltext); } } // Log inbound values if (loggerInbound.isDebugEnabled()) { StringBuilder sb = new StringBuilder(values.size()*64); sb.append("\n") .append("Inbound audit values:"); for (Map.Entry entry : values.entrySet()) { String pathElement = entry.getKey(); String path = AuditApplication.buildPath(rootPath, pathElement); Serializable value = entry.getValue(); sb.append("\n\t").append(path).append("=").append(value); } loggerInbound.debug(sb.toString()); } // Build the key paths using the session root path Map pathedValues = new HashMap(values.size() * 2); for (Map.Entry entry : values.entrySet()) { String pathElement = entry.getKey(); String path = AuditApplication.buildPath(rootPath, pathElement); pathedValues.put(path, entry.getValue()); } // Translate the values map PathMapper pathMapper = auditModelRegistry.getAuditPathMapper(); final Map mappedValues = pathMapper.convertMap(pathedValues); if (mappedValues.isEmpty()) { return mappedValues; } // We have something to record. Start a transaction, if necessary TxnReadState txnState = AlfrescoTransactionSupport.getTransactionReadState(); switch (txnState) { case TXN_NONE: case TXN_READ_ONLY: // New transaction RetryingTransactionCallback> callback = new RetryingTransactionCallback>() { public Map execute() throws Throwable { return recordAuditValuesImpl(mappedValues); } }; RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); txnHelper.setForceWritable(true); return txnHelper.doInTransaction(callback, false, true); case TXN_READ_WRITE: return recordAuditValuesImpl(mappedValues); default: throw new IllegalStateException("Unknown txn state: " + txnState); } } /** * {@inheritDoc} * @since 3.2 */ public Map recordAuditValuesImpl(Map mappedValues) { // Group the values by root path 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); } rootKeyMappedValues.put(path, entry.getValue()); } Map allAuditedValues = new HashMap(mappedValues.size()*2+1); // Now audit for each of the root keys for (Map.Entry> entry : mappedValuesByRootKey.entrySet()) { String rootKey = entry.getKey(); Map rootKeyMappedValues = entry.getValue(); // Get the application AuditApplication application = auditModelRegistry.getAuditApplicationByKey(rootKey); if (application == null) { // There is no application that uses the root key logger.debug( "There is no application for root key: " + rootKey); continue; } // Get the disabled paths Set disabledPaths = getDisabledPaths(application); // Do a quick elimination if the root path is disabled if (disabledPaths.contains(AuditApplication.buildPath(rootKey))) { // The root key has been disabled for this application if (logger.isDebugEnabled()) { logger.debug( "Audit values root path has been excluded by disabled paths: \n" + " Application: " + application + "\n" + " Root Path: " + AuditApplication.buildPath(rootKey)); } continue; } // Do the audit Map rootKeyAuditValues = audit(application, disabledPaths, rootKeyMappedValues); allAuditedValues.putAll(rootKeyAuditValues); } // Done return allAuditedValues; } /** * Audit values for a given application. No path checking is done. * * @param application the audit application to audit to * @param disabledPaths the application's disabled paths * @param values the values to store keyed by full paths. * @return Returns all values as audited */ private Map audit( final AuditApplication application, Set disabledPaths, final Map values) { // Get the model ID for the application Long applicationId = application.getApplicationId(); if (applicationId == null) { throw new AuditException("No persisted instance exists for audit application: " + application); } // Check if there is anything to audit if (values.size() == 0) { if (logger.isDebugEnabled()) { logger.debug( "Audit values have all been excluded by disabled paths: \n" + " Application: " + application + "\n" + " Values: " + values); } return Collections.emptyMap(); } Set generatorKeys = values.keySet(); // Eliminate any paths that have been disabled Iterator generatorKeysIterator = generatorKeys.iterator(); while(generatorKeysIterator.hasNext()) { String generatorKey = generatorKeysIterator.next(); for (String disabledPath : disabledPaths) { if (generatorKey.startsWith(disabledPath)) { // The pathed value is excluded generatorKeysIterator.remove(); } } } // Generate data Map generators = application.getDataGenerators(generatorKeys); Map auditData = generateData(generators); // MNT-8401 for (Map.Entry value : auditData.entrySet()) { String root = value.getKey(); int index = root.lastIndexOf("/"); Map argc = new HashMap(1); argc.put(root.substring(index, root.length()).substring(1), value.getValue()); if (!auditFilter.accept(root.substring(0, index), argc)) { return Collections.emptyMap(); } } // Now extract values Map extractedData = AuthenticationUtil.runAs(new RunAsWork>() { public Map doWork() throws Exception { return extractData(application, values); } }, AuthenticationUtil.getSystemUserName()); // Combine extracted and generated values (extracted data takes precedence) auditData.putAll(extractedData); // Time and username are intrinsic long time = System.currentTimeMillis(); String username = AuthenticationUtil.getFullyAuthenticatedUser(); Long entryId = null; if (!auditData.isEmpty()) { // Persist the values (if not just gathering data in a pre call for use in a post call) boolean justGatherPreCallData = application.isApplicationJustGeneratingPreCallData(); if (!justGatherPreCallData) { entryId = auditDAO.createAuditEntry(applicationId, time, username, auditData); } // Done if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append( ((justGatherPreCallData) ? "\nPreCallData: \n" : "\nNew audit entry: \n") + "\tApplication ID: " + applicationId + "\n" + ((justGatherPreCallData) ? "" : "\tEntry ID: " + entryId + "\n") + "\tValues: " + "\n"); for (Map.Entry entry : values.entrySet()) { sb.append("\t\t").append(entry).append("\n"); } sb.append("\n\tAudit Data: \n"); for (Map.Entry entry : auditData.entrySet()) { sb.append("\t\t").append(entry).append("\n"); } logger.debug(sb.toString()); } } else { // Done ... nothing if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append( "\nNothing audited: \n" + "\tApplication ID: " + applicationId + "\n" + "\tEntry ID: " + entryId + "\n" + "\tValues: " + "\n"); for (Map.Entry entry : values.entrySet()) { sb.append("\t\t").append(entry).append("\n"); } logger.debug(sb.toString()); } } return auditData; } /** * Extracts data from a given map using data extractors from the given application. * * @param application the application providing the data extractors * @param values the data values from which to generate data * @return Returns a map of derived data keyed by full path * * @since 3.2 */ private Map extractData( AuditApplication application, Map values) { Map newData = new HashMap(values.size()); List extractors = application.getDataExtractors(); for (DataExtractorDefinition extractorDef : extractors) { DataExtractor extractor = extractorDef.getDataExtractor(); String triggerPath = extractorDef.getDataTrigger(); String sourcePath = extractorDef.getDataSource(); String targetPath = extractorDef.getDataTarget(); // Check if it is triggered if (!values.containsKey(triggerPath)) { continue; // It is not triggered } // We observe the key, not the actual value if (!values.containsKey(sourcePath)) { continue; // There is no data to extract } Serializable value = values.get(sourcePath); // Check if the extraction is supported if (!extractor.isSupported(value)) { continue; } // Use the extractor to pull the value out final Serializable data; try { data = extractor.extractData(value); } catch (Throwable e) { throw new AlfrescoRuntimeException( "Failed to extract audit data: \n" + " Path: " + sourcePath + "\n" + " Raw value: " + value + "\n" + " Extractor: " + extractor, e); } // Add it to the map newData.put(targetPath, data); } // Done if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append( "\nExtracted audit data: \n" + "\tApplication: " + application + "\n" + "\tValues: " + "\n"); for (Map.Entry entry : values.entrySet()) { sb.append("\t\t").append(entry).append("\n"); } sb.append("\n\tNew Data: \n"); for (Map.Entry entry : newData.entrySet()) { sb.append("\t\t").append(entry).append("\n"); } logger.debug(sb.toString()); } return newData; } /** * @param generators the data generators * @return Returns a map of generated data keyed by full path * * @since 3.2 */ private Map generateData(Map generators) { Map newData = new HashMap(generators.size() + 5); for (Map.Entry entry : generators.entrySet()) { String path = entry.getKey(); DataGenerator generator = entry.getValue(); final Serializable data; try { data = generator.getData(); } catch (Throwable e) { throw new AlfrescoRuntimeException( "Failed to generate audit data: \n" + " Path: " + path + "\n" + " Generator: " + generator, e); } // Add it to the map newData.put(path, data); } // Done return newData; } /** * {@inheritDoc} */ public void auditQuery(AuditQueryCallback callback, AuditQueryParameters parameters, int maxResults) { ParameterCheck.mandatory("callback", callback); ParameterCheck.mandatory("parameters", parameters); // Shortcuts if (parameters.isZeroResultQuery()) { return; } auditDAO.findAuditEntries(callback, parameters, maxResults); } }