diff --git a/config/alfresco/audit-services-context.xml b/config/alfresco/audit-services-context.xml
index 53b3dd62fa..6e9618fdc3 100644
--- a/config/alfresco/audit-services-context.xml
+++ b/config/alfresco/audit-services-context.xml
@@ -30,11 +30,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -74,7 +95,7 @@
-
+
diff --git a/config/alfresco/audit/alfresco-audit-3.2.xsd b/config/alfresco/audit/alfresco-audit-3.2.xsd
index 5b8d205366..62d21e69be 100644
--- a/config/alfresco/audit/alfresco-audit-3.2.xsd
+++ b/config/alfresco/audit/alfresco-audit-3.2.xsd
@@ -120,7 +120,7 @@
-
+
@@ -128,7 +128,7 @@
-
+
diff --git a/config/alfresco/audit/alfresco-audit-access.xml b/config/alfresco/audit/alfresco-audit-access.xml
new file mode 100644
index 0000000000..4da6bf464c
--- /dev/null
+++ b/config/alfresco/audit/alfresco-audit-access.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/alfresco/authentication-services-context.xml b/config/alfresco/authentication-services-context.xml
index 453ce4b47b..d14e6bb2ce 100644
--- a/config/alfresco/authentication-services-context.xml
+++ b/config/alfresco/authentication-services-context.xml
@@ -542,32 +542,45 @@
-
+
+
-
-
-
+
+
+ org.alfresco.repo.security.authentication.TicketComponent
-
-
-
- ${authentication.ticket.validDuration}
+
+
+
+
+
+
+
+
+ ${authentication.ticket.validDuration}
+
+
+
+ ${authentication.ticket.ticketsExpire}
+
+
+
+ false
+
+
+
+
+
+ ${authentication.ticket.expiryMode}
+
+
-
-
- ${authentication.ticket.ticketsExpire}
-
-
-
- false
-
-
-
-
-
- ${authentication.ticket.expiryMode}
+
+
+
+
diff --git a/config/alfresco/avm-services-context.xml b/config/alfresco/avm-services-context.xml
index 058a3c9a38..8977a758df 100644
--- a/config/alfresco/avm-services-context.xml
+++ b/config/alfresco/avm-services-context.xml
@@ -67,15 +67,12 @@
50
-
- 1000
-
-
-
-
+
+
+
diff --git a/config/alfresco/messages/discussion-messages_it.properties b/config/alfresco/messages/discussion-messages_it.properties
index 33b71e431e..81b6d11da2 100755
--- a/config/alfresco/messages/discussion-messages_it.properties
+++ b/config/alfresco/messages/discussion-messages_it.properties
@@ -1,3 +1,3 @@
# Discussion-related messages
-discussion.discussion_for={0}discussione
+discussion.discussion_for={0}discussione
diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml
index da1729d3b2..90e2a213eb 100644
--- a/config/alfresco/public-services-context.xml
+++ b/config/alfresco/public-services-context.xml
@@ -31,8 +31,8 @@
-
-
+
+
diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties
index fa3d6aa0ac..a8258b6afe 100644
--- a/config/alfresco/repository.properties
+++ b/config/alfresco/repository.properties
@@ -323,10 +323,18 @@ db.pool.abandoned.log=false
# Audit configuration
audit.enabled=true
audit.tagging.enabled=true
+audit.alfresco-access.enabled=false
+audit.alfresco-access.sub-events.enabled=false
audit.cmischangelog.enabled=false
audit.dod5015.enabled=false
# 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
+audit.filter.alfresco-access.default.enabled=true
+audit.filter.alfresco-access.transaction.user=~System;~null;.*
+audit.filter.alfresco-access.transaction.type=cm:folder;cm:content
+audit.filter.alfresco-access.transaction.path=~/sys:archivedItem;~/ver:;.*
+
# System Configuration
system.store=system://system
diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java
index 142ac89dbe..24a29a59ce 100644
--- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java
+++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java
@@ -2566,6 +2566,7 @@ public class ContentDiskDriver extends AlfrescoDiskDriver implements DiskInterfa
if (permissionService.hasPermission((NodeRef) finalFileState.getFilesystemObject(), PermissionService.WRITE_PROPERTIES) == AccessStatus.ALLOWED)
{
+ nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIER, authService.getCurrentUserName());
Date modifyDate = new Date(finalFileState.getModifyDateTime());
nodeService.setProperty(nodeRef, ContentModel.PROP_MODIFIED, modifyDate);
diff --git a/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java b/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java
index fb4cbbbf94..906ffc2720 100644
--- a/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java
+++ b/source/java/org/alfresco/repo/audit/AnnotationTestInterface.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -26,7 +26,6 @@ import org.alfresco.service.PublicService;
*
* @author Andy Hind
*/
-@PublicService
public interface AnnotationTestInterface
{
@Auditable()
diff --git a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java
index 5a6b3829f9..9c6d8af741 100644
--- a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java
+++ b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -71,6 +71,7 @@ public class AuditComponentImpl implements AuditComponent
private PropertyValueDAO propertyValueDAO;
private AuditDAO auditDAO;
private TransactionService transactionService;
+ private AuditFilter auditFilter;
/**
* Default constructor
@@ -113,6 +114,14 @@ public class AuditComponentImpl implements AuditComponent
{
this.transactionService = transactionService;
}
+
+ /**
+ * Set the component used to filter which audit events to record
+ */
+ public void setAuditFilter(AuditFilter auditFilter)
+ {
+ this.auditFilter = auditFilter;
+ }
/**
* {@inheritDoc}
@@ -487,7 +496,7 @@ public class AuditComponentImpl implements AuditComponent
ParameterCheck.mandatory("rootPath", rootPath);
AuditApplication.checkPathFormat(rootPath);
- if (values == null || values.isEmpty() || !areAuditValuesRequired())
+ if (values == null || values.isEmpty() || !areAuditValuesRequired() || !auditFilter.accept(rootPath, values))
{
return Collections.emptyMap();
}
diff --git a/source/java/org/alfresco/repo/audit/AuditFilter.java b/source/java/org/alfresco/repo/audit/AuditFilter.java
new file mode 100644
index 0000000000..2e093021e7
--- /dev/null
+++ b/source/java/org/alfresco/repo/audit/AuditFilter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2005-2011 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.Map;
+
+import org.alfresco.repo.audit.model.AuditApplication;
+import org.alfresco.repo.audit.model._3.AuditPath;
+
+/**
+ * Filter of audit map values before an audit record is written.
+ *
+ * @author Alan Davis
+ */
+public interface AuditFilter
+{
+ /**
+ * Returns {@code true} if the audit map values have not been discarded by audit filters.
+ * @param rootPath String a base path of {@link AuditPath} key entries concatenated with the
+ * path separator '/' ({@link AuditApplication#AUDIT_PATH_SEPARATOR})
+ * @param auditMap Map of values to audit, mapped by {@link AuditPath} key relative to root path.
+ * @return {@code true} if the audit map values should be recorded.
+ */
+ boolean accept(String rootPath, Map auditMap);
+}
\ No newline at end of file
diff --git a/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java b/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java
index dfd6c1dc2f..c61923da3a 100644
--- a/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java
+++ b/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -89,7 +89,7 @@ public class AuditMethodInterceptor implements MethodInterceptor
private static final Log logger = LogFactory.getLog(AuditMethodInterceptor.class);
- private PublicServiceIdentifier publicServiceIdentifier;
+ private BeanIdentifier beanIdentifier;
private AuditComponent auditComponent;
private TransactionService transactionService;
@@ -112,9 +112,9 @@ public class AuditMethodInterceptor implements MethodInterceptor
logger.warn("Property 'useNewConfig' is no longer used.");
}
- public void setPublicServiceIdentifier(PublicServiceIdentifier serviceIdentifier)
+ public void setBeanIdentifier(BeanIdentifier beanIdentifier)
{
- this.publicServiceIdentifier = serviceIdentifier;
+ this.beanIdentifier = beanIdentifier;
}
public void setAuditComponent(AuditComponent auditComponent)
@@ -185,7 +185,7 @@ public class AuditMethodInterceptor implements MethodInterceptor
Object[] args = mi.getArguments();
Map namedArguments = getInvocationArguments(auditableDef, args);
// Get the service name
- String serviceName = publicServiceIdentifier.getPublicServiceName(mi);
+ String serviceName = beanIdentifier.getBeanName(mi);
if (serviceName == null)
{
// Not a public service
@@ -338,10 +338,22 @@ public class AuditMethodInterceptor implements MethodInterceptor
final String methodName,
final Map namedArguments)
{
- final String rootPath = AuditApplication.buildPath(AUDIT_PATH_API_PRE, serviceName, methodName, AUDIT_SNIPPET_ARGS);
+ String rootPath;
+ Map auditData;
+ if (namedArguments == null || namedArguments.isEmpty())
+ {
+ rootPath = AuditApplication.buildPath(AUDIT_PATH_API_PRE, serviceName, methodName);
+ auditData = new HashMap(1);
+ auditData.put(AUDIT_SNIPPET_ARGS, null);
+ }
+ else
+ {
+ rootPath = AuditApplication.buildPath(AUDIT_PATH_API_PRE, serviceName, methodName, AUDIT_SNIPPET_ARGS);
+ auditData = namedArguments;
+ }
// Audit in a read-write txn
- Map auditedData = auditComponent.recordAuditValues(rootPath, namedArguments);
+ Map auditedData = auditComponent.recordAuditValues(rootPath, auditData);
// Done
if (logger.isDebugEnabled() && auditedData.size() > 0)
{
@@ -369,13 +381,20 @@ public class AuditMethodInterceptor implements MethodInterceptor
final String rootPath = AuditApplication.buildPath(AUDIT_PATH_API_POST, serviceName, methodName);
final Map auditData = new HashMap(23);
- for (Map.Entry entry : namedArguments.entrySet())
+ if (namedArguments.isEmpty())
{
- String argName = entry.getKey();
- Serializable argValue = entry.getValue();
- auditData.put(
- AuditApplication.buildPath(AUDIT_SNIPPET_ARGS, argName),
- argValue);
+ auditData.put(AUDIT_SNIPPET_ARGS, null);
+ }
+ else
+ {
+ for (Map.Entry entry : namedArguments.entrySet())
+ {
+ String argName = entry.getKey();
+ Serializable argValue = entry.getValue();
+ auditData.put(
+ AuditApplication.buildPath(AUDIT_SNIPPET_ARGS, argName),
+ argValue);
+ }
}
if (ret != null)
{
diff --git a/source/java/org/alfresco/repo/audit/PublicServiceIdentifier.java b/source/java/org/alfresco/repo/audit/BeanIdentifier.java
similarity index 63%
rename from source/java/org/alfresco/repo/audit/PublicServiceIdentifier.java
rename to source/java/org/alfresco/repo/audit/BeanIdentifier.java
index cb0d5592e1..5c6956c575 100644
--- a/source/java/org/alfresco/repo/audit/PublicServiceIdentifier.java
+++ b/source/java/org/alfresco/repo/audit/BeanIdentifier.java
@@ -21,18 +21,19 @@ package org.alfresco.repo.audit;
import org.aopalliance.intercept.MethodInvocation;
/**
- * This defines the API to identify the public service upon which a method invocation has been made.
+ * Identify a bean upon which a method invocation has been made. Originally
+ * this was only public services but has been relaxed to be any bean.
*
- * @author Andy Hind
+ * @author Andy Hind, David Ward, Alan Davis
*/
-public interface PublicServiceIdentifier
+public interface BeanIdentifier
{
/**
- * Get the name of the public service for the method invocation.
+ * Get the name of the bean (normally a service) for the method invocation.
*
* @param mi the method invocation
- * @return Returns the name of the public service or null if it is
- * not recognized as a public service
+ * @return Returns the name of the bean or null if it is
+ * not recognized
*/
- public String getPublicServiceName(MethodInvocation mi);
+ public String getBeanName(MethodInvocation mi);
}
diff --git a/source/java/org/alfresco/repo/audit/BeanIdentifierImpl.java b/source/java/org/alfresco/repo/audit/BeanIdentifierImpl.java
new file mode 100644
index 0000000000..8f8db97206
--- /dev/null
+++ b/source/java/org/alfresco/repo/audit/BeanIdentifierImpl.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2005-2011 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.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.alfresco.service.Auditable;
+import org.aopalliance.intercept.MethodInvocation;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.aop.ProxyMethodInvocation;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.ListableBeanFactory;
+
+/**
+ * Lookup the name of a bean that is being audited by {@link AuditMethodInterceptor}.
+ *
+ * Originally used to look up public services annotated with {@code @PublicService},
+ * but has now been relaxed to be any bean that uses a proxy. For the method to be
+ * audited it still needs to be annotated with {@code @Auditable}.
+ *
+ * @author Andy Hind, David Ward, Alan Davis
+ */
+public class BeanIdentifierImpl implements BeanIdentifier, BeanFactoryAware
+{
+ private static Log s_logger = LogFactory.getLog(BeanIdentifierImpl.class);
+ private static ThreadLocal> methodToBeanMap =
+ new ThreadLocal>();
+
+ private ListableBeanFactory beanFactory;
+
+ public BeanIdentifierImpl()
+ {
+ super();
+ }
+
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException
+ {
+ this.beanFactory = (ListableBeanFactory)beanFactory;
+ }
+
+ /**
+ * {@inheritDoc}
+ * Cache service name look up.
+ */
+ public String getBeanName(MethodInvocation mi)
+ {
+ return getName(mi);
+ }
+
+ private String getName(MethodInvocation mi) throws BeansException
+ {
+ if (methodToBeanMap.get() == null)
+ {
+ methodToBeanMap.set(new HashMap());
+ }
+ Method method = mi.getMethod();
+ String name = methodToBeanMap.get().get(method);
+ if (name == null)
+ {
+ name = getBeanNameImpl(mi);
+ methodToBeanMap.get().put(method, name);
+ }
+ else
+ {
+ if (s_logger.isDebugEnabled())
+ {
+ s_logger.debug("Cached look up for " + name + "." + method.getName());
+ }
+ }
+ return name;
+ }
+
+ /**
+ * Do the look up by interface type.
+ *
+ * @return Returns the name of the service or null if not found
+ */
+ private String getBeanNameImpl(MethodInvocation mi) throws BeansException
+ {
+ if (mi instanceof ProxyMethodInvocation)
+ {
+ Object proxy = ((ProxyMethodInvocation) mi).getProxy();
+ Map beans = beanFactory.getBeansOfType(proxy.getClass());
+ Iterator iter = beans.entrySet().iterator();
+ while (iter.hasNext())
+ {
+ Map.Entry entry = (Map.Entry) iter.next();
+ String name = (String) entry.getKey();
+ if (proxy == entry.getValue() && !name.equals("DescriptorService"))
+ {
+ return name;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/source/java/org/alfresco/repo/audit/PropertyAuditFilter.java b/source/java/org/alfresco/repo/audit/PropertyAuditFilter.java
new file mode 100644
index 0000000000..b4a90f14f1
--- /dev/null
+++ b/source/java/org/alfresco/repo/audit/PropertyAuditFilter.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2005-2011 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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.WeakHashMap;
+import java.util.regex.Pattern;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Filter using property file values to accept or reject audit map values.
+ *
+ * The last component in the {@code rootPath} is considered to be the event
+ * action. The keys in an audit map identify each audit value. Properties may be
+ * defined to accept or reject each value. If any value in an audit map is
+ * rejected, the whole map is rejected. So that one does not have to define
+ * too many properties, a 'default' event action property may be defined. This
+ * will be inherited by all actions unless a property is defined for a particular
+ * event action. For example:
+ *
+ *
+ * Each property value defines a list of regular expressions that will be used
+ * to match the actual audit map values. In the above example, events created
+ * by any user except for the internal user 'System' will be recorded by default
+ * for all event actions. However the property for the 'transaction' event action
+ * overrides this to record even 'System' events.
+ *
+ * For any filters to be applied to an event action, that action's filters must be
+ * enabled with an 'enabled' property set to {@code "true"}. However this may
+ * also be done by using the 'default' event action, as shown above.
+ *
+ * Note: Property names have a {@code "audit.filter."} prefix and use {@code '.'}
+ * as a separator where as components of rootPath and keys in the audit map use
+ * {@code '/'}. The following is an example rootPath and audit map which could be
+ * used with the corresponding property names shown above:
+ *
+ *
+ *
+ * Lists are evaluated from left to right allowing one flexibility to accept or
+ * reject different combinations of values. If no match is made by the end of the
+ * list the value is rejected. If there is not a property for a given value or
+ * an empty list is defined (as above for the user value on a transaction action)
+ * any value is accepted.
+ *
+ * Each regular expression in the list is separated by a {@code ';'}. Expressions
+ * that include a {@code ';'} may be escaped using a {@code '\'}. An expression
+ * that starts with a {@code '~'} indicates that any matching value should be
+ * rejected. If the first character of an expression needs to be a {@code '~'} it
+ * too may be escaped with a {@code '\'}.
+ *
+ * A property value may be a reference to another property, which saves having
+ * multiple copies. This is indicated by a {@code '$' as the first character of the
+ * property value. If the first character of an expression needs to be a
+ * {@code '$'} it too may be escaped with a {@code '\'}. For example:
+ *
+ *
+ * @author Alan Davis
+ */
+public class PropertyAuditFilter implements AuditFilter
+{
+ private static Log logger = LogFactory.getLog(PropertyAuditFilter.class);
+
+ private static final char NOT = '~';
+ private static final char REDIRECT = '$';
+ private static final String REG_EXP_SEPARATOR = ";";
+ private static final char PROPERTY_SEPARATOR = '.';
+ private static final String PROPERY_NAME_PREFIX = "audit.filter";
+ private static final char ESCAPE = '\\';
+
+ private static final String ESCAPED_REDIRECT = ""+ESCAPE+REDIRECT;
+ private static final String ESCAPED_REG_EXP_SEPARATOR = ""+ESCAPE+REG_EXP_SEPARATOR;
+ private static final String ESCAPED_NOT = ""+ESCAPE+NOT;
+
+ private static final String ENABLED = "enabled";
+ private static final String DEFAULT = "default";
+
+ /**
+ * Cache of {@code Patterns} for performance.
+ */
+ static Map patternCache =
+ Collections.synchronizedMap(new WeakHashMap());
+
+ /**
+ * Properties to drive the filter.
+ */
+ Properties properties;
+
+ /**
+ * Set the properties object holding filter configuration
+ * @since 3.2
+ */
+ public void setProperties(Properties properties)
+ {
+ this.properties = properties;
+ }
+
+ /**
+ * @inheritDoc
+ * @param @inheritDoc
+ * @param @inheritDoc
+ * @return @inheritDoc
+ */
+ @Override
+ public boolean accept(String rootPath, Map auditMap)
+ {
+ String[] root = splitPath(rootPath);
+ String rootProperty = getPropertyName(PROPERY_NAME_PREFIX, getPropertyName(root));
+ String defaultRootProperty = getDefaultRootProperty(root);
+
+ if ("true".equalsIgnoreCase(getProperty(rootProperty, defaultRootProperty, ENABLED)))
+ {
+ for (Map.Entry entry : auditMap.entrySet())
+ {
+ Serializable value = entry.getValue();
+ if (value == null)
+ {
+ value = "null";
+ }
+ String stringValue = (value instanceof String) ? (String)value : value.toString();
+ String[] key = splitPath(entry.getKey());
+ String propertyValue = getProperty(rootProperty, defaultRootProperty, key);
+ if (!acceptValue(stringValue, propertyValue, rootProperty, key))
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Rejected \n\t "+rootPath+'/'+entry.getKey()+"="+stringValue+
+ "\n\t"+getPropertyName(rootProperty, getPropertyName(key))+"="+propertyValue);
+ }
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks a single value against a list of regular expressions.
+ */
+ private boolean acceptValue(String value, String regExpValue, String rootProperty, String... key)
+ {
+ // If no property or zero length it matches.
+ if (regExpValue == null || regExpValue.length() == 0)
+ {
+ return true;
+ }
+
+ for (String regExp: getRegExpList(regExpValue, rootProperty, key))
+ {
+ boolean includeExp = regExp.charAt(0) != NOT;
+ if (!includeExp || regExp.startsWith(ESCAPED_NOT))
+ {
+ regExp = regExp.substring(1);
+ }
+ if (getPattern(regExp).matcher(value).matches())
+ {
+ return includeExp;
+ }
+ }
+
+ return false;
+ }
+
+ private Pattern getPattern(String regExp)
+ {
+ Pattern pattern = patternCache.get(regExp);
+ if (pattern == null)
+ {
+ pattern = Pattern.compile(regExp);
+ patternCache.put(regExp, pattern);
+ }
+ return pattern;
+ }
+
+ /**
+ * @return the root property name for the default event action.
+ */
+ private String getDefaultRootProperty(String[] root)
+ {
+ String action = root[root.length-1];
+ root[root.length-1] = DEFAULT;
+ String defaultRootProperty = getPropertyName(PROPERY_NAME_PREFIX, getPropertyName(root));
+ root[root.length-1] = action;
+ return defaultRootProperty;
+ }
+
+ /**
+ * @return the value of the property {@code rootProperty+'.'+getPropertyName(keyComponents)}
+ * defaulting to {@code defaultRootProperty+'.'+getPropertyName(keyComponents)}.
+ */
+ private String getProperty(String rootProperty, String defaultRootProperty, String... keyComponents)
+ {
+ String keyName = getPropertyName(keyComponents);
+ String propertyName = getPropertyName(rootProperty, keyName);
+ String value = getProperty(null, propertyName);
+ if (value == null)
+ {
+ value = getProperty(null, getPropertyName(defaultRootProperty, keyName));
+ }
+ return value;
+ }
+
+ /**
+ * @return a property value, including redirected values (where the value
+ * of a property starts with a {@code '$'} indicating it is another property
+ * name).
+ * @throws IllegalArgumentException if redirecting properties reference themselves.
+ */
+ private String getProperty(List loopCheck, String propertyName)
+ {
+ String value = properties.getProperty(propertyName);
+
+ // Handle redirection of properties.
+ if (value != null && value.length() > 0 && value.charAt(0) == REDIRECT)
+ {
+ String newPropertyName = value.substring(1);
+ if (loopCheck == null)
+ {
+ loopCheck = new ArrayList();
+ }
+ if (loopCheck.contains(newPropertyName))
+ {
+ RuntimeException e = new IllegalArgumentException("Redirected property "+
+ newPropertyName+" referes back to itself.");
+ logger.error("Error found in properties for audit filter.", e);
+ throw e;
+ }
+ loopCheck.add(propertyName);
+ value = getProperty(loopCheck, newPropertyName);
+ }
+ else if (value == null && loopCheck != null && !loopCheck.isEmpty())
+ {
+ RuntimeException e = new IllegalArgumentException("Redirected property "+
+ loopCheck.get(loopCheck.size()-1)+
+ " points to "+propertyName+" but it does not exist.");
+ logger.error("Error found in properties for audit filter.", e);
+ throw e;
+ }
+
+ return value;
+ }
+
+ /**
+ * Returns a List of regular expressions from a property's String value.
+ * A leading {@code '~'} indicating the regular expression should be used
+ * to reject values. This may be escaped with a leading back slash
+ * ({@code "\\~"}) if the first character must be a semicolon. Other
+ * escape characters are removed. A check is made that no expression is
+ * zero length.
+ * @return a List of regular expressions.
+ * @throws IllegalArgumentException if there are any zero length expressions.
+ */
+ private List getRegExpList(String value, String rootProperty, String... key)
+ {
+ // Split the value into substrings separated by ';'. This may be escaped using "\;".
+ List regExpList = new ArrayList();
+ {
+ int j = 0;
+ int i = j - 1;
+ do
+ {
+ i = value.indexOf(';', i+1);
+ if (i != -1)
+ {
+ if (i == 0 || value.charAt(i-1) != '\\')
+ {
+ regExpList.add(value.substring(j, i));
+ j = i + 1;
+ }
+ }
+ }
+ while (i != -1);
+ if (j < value.length()-1)
+ {
+ regExpList.add(value.substring(j));
+ }
+ }
+
+ // Remove escape characters other than the NOT (\~)
+ // \$ at the start becomes "$"
+ // \; anywhere becomes ";"
+ for (int i=regExpList.size()-1; i >= 0; i--)
+ {
+ String regExp = regExpList.get(i);
+ if (regExp.startsWith(ESCAPED_REDIRECT))
+ {
+ regExp = regExp.substring(1);
+ }
+ regExp = regExp.replaceAll(ESCAPED_REG_EXP_SEPARATOR, REG_EXP_SEPARATOR);
+
+ if (regExp.length() == 0 || (regExp.charAt(0) == NOT && regExp.length() == 1))
+ {
+ throw new IllegalArgumentException(getPropertyName(rootProperty, getPropertyName(key))+"="+value+
+ "includes an empty regular expression.");
+ }
+ regExpList.set(i, regExp);
+ }
+ return regExpList;
+ }
+
+ /**
+ * @return a property name from the supplied components. Each component is
+ * separated by a {@code '.'}.
+ */
+ private String getPropertyName(String... components)
+ {
+ StringBuilder sb = new StringBuilder();
+ for (String component: components)
+ {
+ if (sb.length() > 0)
+ {
+ sb.append(PROPERTY_SEPARATOR);
+ }
+ sb.append(component);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * @return a list of components separated by '/' characters.
+ */
+ private String[] splitPath(String path)
+ {
+ if (path.length() > 0 && path.charAt(0) == '/')
+ {
+ path = path.substring(1);
+ }
+ return path.split("/");
+ }
+}
diff --git a/source/java/org/alfresco/repo/audit/PropertyAuditFilterTest.java b/source/java/org/alfresco/repo/audit/PropertyAuditFilterTest.java
new file mode 100644
index 0000000000..c310995a76
--- /dev/null
+++ b/source/java/org/alfresco/repo/audit/PropertyAuditFilterTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2005-2011 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 static org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * A low level unit test of the filter on audit maps.
+ *
+ * @author Alan Davis
+ */
+public class PropertyAuditFilterTest
+{
+ private PropertyAuditFilter filter;
+ private Properties properties;
+
+ private String rootPath;
+ private Map auditMap;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ filter = new PropertyAuditFilter();
+ properties = new Properties();
+ filter.setProperties(properties);
+
+ rootPath = "root/action";
+ auditMap = new HashMap();
+ auditMap.put("name", "value");
+ }
+
+ @Test
+ public void testNoFilterIfNoProperties()
+ {
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertTrue("Filter should only run if properties are set.", actual);
+ }
+
+ @Test
+ public void testNoRegexOnValue()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertTrue("Value should have been accepted.", actual);
+ }
+
+ @Test
+ public void testRegexOnValue()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "value");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertTrue("Value should have been accepted.", actual);
+ }
+
+ @Test
+ public void testRegexOnBadValue()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "~value");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertFalse("Value should have been rejected.", actual);
+ }
+
+ @Test
+ public void testNullValue()
+ {
+ auditMap.put("name", null);
+
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "null");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertTrue("A null value should match null", actual);
+ }
+
+ @Test
+ public void testNullStringValue()
+ {
+ auditMap.put("name", "null");
+
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "null");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertTrue("A null value should match null", actual);
+ }
+
+ @Test
+ public void testNonStringValue()
+ {
+ LinkedHashSet value = new LinkedHashSet();
+ value.add(Integer.valueOf(1));
+ value.add(Integer.valueOf(2));
+ value.add(Integer.valueOf(3));
+ auditMap.put("name", value);
+
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "\\[1, 2, 3\\]");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertTrue("The check should have worked on the value.toString().", actual);
+ }
+
+ @Test
+ public void testZeroLengthRegex()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertTrue("Should match any values just like having no property", actual);
+ }
+
+ @Test
+ public void testDefaultActionUsedAsFallback()
+ {
+ properties.put("audit.filter.root.default.enabled", "true");
+ properties.put("audit.filter.root.default.name", "~value");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertFalse("The 'default' fallback action should have been used to " +
+ "enable the filter and reject the value.", actual);
+ }
+
+ @Test
+ public void testRedirect()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "$anotherProperty");
+ properties.put("anotherProperty", "$theFinalProperty");
+ properties.put("theFinalProperty", "~value");
+
+ boolean actual = filter.accept(rootPath, auditMap);
+ assertFalse("Redirected properties should have rejected the value.", actual);
+ }
+
+ @Test
+ public void testMultipleRegExp()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "beGood;~b.*;.*");
+
+ auditMap.put("name", "beGood");
+ assertTrue("Should match 1st regex", filter.accept(rootPath, auditMap));
+
+ auditMap.put("name", "bad");
+ assertFalse("Should match 2nd regex", filter.accept(rootPath, auditMap));
+
+ auditMap.put("name", "value");
+ assertTrue("Should match 3rd regex", filter.accept(rootPath, auditMap));
+ }
+
+ @Test
+ public void testMultipleRegExpWithNoCatchAll()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "beGood;~b.*");
+
+ auditMap.put("name", "value");
+ assertFalse("Should match nothing", filter.accept(rootPath, auditMap));
+ }
+
+ @Test
+ public void testEscapedSemicolon()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "value\\\\;value");
+
+ auditMap.put("name", "value\\;value");
+ assertTrue("Should match 1st regex", filter.accept(rootPath, auditMap));
+ }
+
+ @Test
+ public void testEscapedRedirect()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "\\$");
+
+ auditMap.put("name", "");
+ assertTrue("Should match only zero length values", filter.accept(rootPath, auditMap));
+ }
+
+ @Test
+ public void testEscapedNot()
+ {
+ properties.put("audit.filter.root.action.enabled", "true");
+ properties.put("audit.filter.root.action.name", "\\~.*");
+
+ auditMap.put("name", "~good");
+ assertTrue("Should match any value starting with '~'.", filter.accept(rootPath, auditMap));
+ }
+}
diff --git a/source/java/org/alfresco/repo/audit/PublicServiceIdentifierImpl.java b/source/java/org/alfresco/repo/audit/PublicServiceIdentifierImpl.java
deleted file mode 100644
index f26100c7fe..0000000000
--- a/source/java/org/alfresco/repo/audit/PublicServiceIdentifierImpl.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2005-2010 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.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-import org.alfresco.service.PublicService;
-import org.aopalliance.intercept.MethodInvocation;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.BeanFactory;
-import org.springframework.beans.factory.BeanFactoryAware;
-import org.springframework.beans.factory.ListableBeanFactory;
-
-/**
- * Identify public services by method invocation. Look ups are cached on a thread local as they are quite expensive.
- * All public service names end with "Service" and start with capital letter.
- * This pattern is used to filter bean names. TODO: Look at pulling out all the mappings at start up.
- *
- * @author Andy Hind
- */
-public class PublicServiceIdentifierImpl implements PublicServiceIdentifier, BeanFactoryAware
-{
- private static Log s_logger = LogFactory.getLog(PublicServiceIdentifierImpl.class);
- private static ThreadLocal> methodToServiceMap = new ThreadLocal>();
-
- private ListableBeanFactory beanFactory;
-
- public PublicServiceIdentifierImpl()
- {
- super();
- }
-
- public void setBeanFactory(BeanFactory beanFactory) throws BeansException
- {
- this.beanFactory = (ListableBeanFactory)beanFactory;
- }
-
- public String getPublicServiceName(MethodInvocation mi)
- {
- return getServiceName(mi);
- }
-
- /**
- * {@inheritDoc}
- * Cache service name look up.
- */
- private String getServiceName(MethodInvocation mi) throws BeansException
- {
- if (methodToServiceMap.get() == null)
- {
- methodToServiceMap.set(new HashMap());
- }
- Method method = mi.getMethod();
- String serviceName = methodToServiceMap.get().get(method);
- if (serviceName == null)
- {
- serviceName = getServiceNameImpl(mi);
- methodToServiceMap.get().put(method, serviceName);
- }
- else
- {
- if (s_logger.isDebugEnabled())
- {
- s_logger.debug("Cached look up for " + serviceName + "." + method.getName());
- }
- }
- return serviceName;
- }
-
- /**
- * Do the look up by interface type.
- *
- * @return Returns the name of the service or null if not found
- */
- @SuppressWarnings("unchecked")
- private String getServiceNameImpl(MethodInvocation mi) throws BeansException
- {
- Class clazz = mi.getThis().getClass();
- while (clazz != null)
- {
- Class[] interfaces = clazz.getInterfaces();
- for (Class iFace : interfaces)
- {
- Class publicServiceInterface = findPublicService(iFace);
- if (publicServiceInterface != null)
- {
- Map beans = beanFactory.getBeansOfType(publicServiceInterface);
- Iterator iter = beans.entrySet().iterator();
- while (iter.hasNext())
- {
- Map.Entry entry = (Map.Entry) iter.next();
- String serviceName = (String) entry.getKey();
- if ((serviceName.endsWith("Service"))
- && (Character.isUpperCase(serviceName.charAt(0)))
- && !serviceName.equals("DescriptorService"))
- {
- return serviceName;
- }
- }
- }
-
- }
- clazz = clazz.getSuperclass();
- }
- return null;
- }
-
- /**
- * We use a marker annotation to identify public interfaces.
- * The interfaces have to be walked to determine if a public interface is implemented.
- *
- * Only one public service interface is expected.
- *
- * @param clazz
- * @return
- */
- @SuppressWarnings("unchecked")
- private Class findPublicService(Class clazz)
- {
- if (!clazz.isInterface())
- {
- return null;
- }
-
- if (clazz.isAnnotationPresent(PublicService.class))
- {
- return clazz;
- }
-
- Class[] classes = clazz.getInterfaces();
- for(Class implemented: classes)
- {
- Class answer = findPublicService(implemented);
- if(answer != null)
- {
- return answer;
- }
- }
- return null;
-
- }
-}
diff --git a/source/java/org/alfresco/repo/audit/access/AccessAuditor.java b/source/java/org/alfresco/repo/audit/access/AccessAuditor.java
new file mode 100644
index 0000000000..ff9b086b2b
--- /dev/null
+++ b/source/java/org/alfresco/repo/audit/access/AccessAuditor.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright (C) 2005-2011 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.access;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.audit.AuditComponent;
+import org.alfresco.repo.audit.model.AuditApplication;
+import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCancelCheckOut;
+import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckIn;
+import org.alfresco.repo.coci.CheckOutCheckInServicePolicies.OnCheckOut;
+import org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy;
+import org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy;
+import org.alfresco.repo.copy.CopyServicePolicies.OnCopyCompletePolicy;
+import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnRemoveAspectPolicy;
+import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
+import org.alfresco.repo.policy.JavaBehaviour;
+import org.alfresco.repo.policy.PolicyComponent;
+import org.alfresco.repo.policy.PolicyScope;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.repo.transaction.TransactionListener;
+import org.alfresco.repo.transaction.TransactionListenerAdapter;
+import org.alfresco.repo.transaction.TransactionalResourceHelper;
+import org.alfresco.repo.version.VersionServicePolicies.OnCreateVersionPolicy;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.PropertyCheck;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * Creates high level audit records on the creation, deletion, modification and access
+ * of content and folders. Lower level events are grouped together by transaction
+ * and node.
+ *
+ * To turn on auditing of these events and sub events add the following property to
+ * alfresco-global.properties:
+ *
+ * # Enable audit in general
+ * audit.enabled=true
+ *
+ * # Enable the alfresco-access audit application
+ * audit.alfresco-access.enabled=true
+ *
+ * # Enable the auditing of sub-actions. Normally disabled as these values are
+ * # not normally needed by audit configurations, but may be useful to developers
+ * audit.alfresco-access.sub-actions.enabled=true
+ *
+ *
+ * The following properties are set by default to discard events where the user is
+ * 'null' or 'System', the node path is '/sys:archivedItem' or under '/ver:' or
+ * the node type is not 'cm:folder' or 'cm:content'. These values result in events
+ * only being recorded if they are initiated by users of the system. These vales may
+ * be overridden if required.
+ *
+ *
+ * Node and Content changes generate the following audit structure. Elements are omitted
+ * if not changed by the transaction. The {@code /sub-action/} structure holds
+ * cut down details of each sub-action, but are only included if the global property
+ * {@code audit.alfresco-access.sub-actions.enabled=true}.
+ *