()
+ {
+ public Void doWork() throws Exception
+ {
+ if (!authenticationService.authenticationExists(getName()))
+ {
+ authenticationService.createAuthentication(getName(), getName().toCharArray());
+ }
+ return null;
+ }
+ };
+ AuthenticationUtil.runAs(createAuthenticationWork, AuthenticationUtil.getSystemUserName());
+
+ // Clear everything out and do a successful authentication
+ auditService.clearAudit(APPLICATION_API_TEST);
+ try
+ {
+ AuthenticationUtil.pushAuthentication();
+ authenticationService.authenticate(getName(), getName().toCharArray());
+ }
+ finally
+ {
+ AuthenticationUtil.popAuthentication();
+ }
+
+ // Check that the call was audited
+ results.clear();
+ sb.delete(0, sb.length());
+ auditService.auditQuery(auditQueryCallback, APPLICATION_API_TEST, null, null, null, -1);
+ logger.debug(sb.toString());
+ assertFalse("Did not get any audit results after successful login", results.isEmpty());
+
+ // Clear everything and check that unsuccessful authentication was audited
+ auditService.clearAudit(APPLICATION_API_TEST);
+ try
+ {
+ authenticationService.authenticate("banana", "****".toCharArray());
+ fail("Invalid authentication attempt should fail");
+ }
+ catch (AuthenticationException e)
+ {
+ // Expected
+ }
+ results.clear();
+ sb.delete(0, sb.length());
+ auditService.auditQuery(auditQueryCallback, APPLICATION_API_TEST, null, null, null, -1);
+ logger.debug(sb.toString());
+ assertFalse("Did not get any audit results after failed login", results.isEmpty());
+ }
}
diff --git a/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java b/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java
index 912ec2303f..6b1d3e8ee6 100644
--- a/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java
+++ b/source/java/org/alfresco/repo/audit/AuditMethodInterceptor.java
@@ -24,33 +24,107 @@
*/
package org.alfresco.repo.audit;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.alfresco.error.StackTraceUtil;
+import org.alfresco.repo.audit.model.AuditApplication;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.service.Auditable;
+import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
+import org.alfresco.service.cmr.repository.datatype.TypeConversionException;
+import org.alfresco.service.transaction.TransactionService;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
/**
* A method interceptor to wrap method invocations with auditing.
+ *
+ * V3.2 Configuration:
+ * As of V3.2, the pre- and post-invocation values are passed to the audit component
+ * for processing. Individual applications have to extract the desired audit values.
+ * Values are audited before and after the invocation so that applications that desire
+ * to extract derived data before the invocation can have a chance to do so; generally,
+ * however, the post-invocation values will be the most useful.
+ *
+ * The values passed to the audit component (assuming auditing is enabled and the
+ * new configuration is being used) are:
+ *
+ * /alfresco-api
+ * /pre
+ * /<service>
+ * /<method>
+ * /args
+ * /<arg-name>=<value>
+ * /<arg-name>=<value>
+ * ...
+ * /service
+ * /post
+ * /<service>
+ * /<method>
+ * /args
+ * /<arg-name>=<value>
+ * /<arg-name>=<value>
+ * ...
+ * /result=<value>
+ * /error=<value>
*
- * A single instance is used to wrap all services. If the single instance is disabled
- * no auditing will be carried out and there will be minimal overhead.
+ *
+ * Applications can remap the paths onto their configurations as appropriate.
+ *
+ * TODO: Audit configuration mapping needs to support conditionals
*
* @author Andy Hind
+ * @author Derek Hulley
*/
public class AuditMethodInterceptor implements MethodInterceptor
{
- //private static Log s_logger = LogFactory.getLog(AuditMethodInterceptor.class);
+ public static final String AUDIT_PATH_API_PRE = "/alfresco-api/pre";
+ public static final String AUDIT_PATH_API_POST = "/alfresco-api/post";
+ public static final String AUDIT_SNIPPET_ARGS = "/args";
+ public static final String AUDIT_SNIPPET_RESULT = "/result";
+ public static final String AUDIT_SNIPPET_ERROR = "/error";
+
+ private static final Log logger = LogFactory.getLog(AuditMethodInterceptor.class);
+ private PublicServiceIdentifier publicServiceIdentifier;
private AuditComponent auditComponent;
+ private TransactionService transactionService;
- private boolean disabled = false;
+ private boolean enabled = false;
+ private boolean useNewConfig = false;
+
+ private final ThreadLocal inAudit = new ThreadLocal();
public AuditMethodInterceptor()
{
super();
}
- public void setDisabled(boolean disabled)
+ /**
+ * Enable or disable auditing at a high level (default: false)
+ */
+ public void setEnabled(boolean enabled)
{
- this.disabled = disabled;
+ this.enabled = enabled;
+ }
+
+ /**
+ * Use the new audit configuration (default: false)
+ *
+ * @param useNewConfig true to use the new audit configuration
+ */
+ public void setUseNewConfig(boolean useNewConfig)
+ {
+ this.useNewConfig = useNewConfig;
+ }
+
+ public void setPublicServiceIdentifier(PublicServiceIdentifier serviceIdentifier)
+ {
+ this.publicServiceIdentifier = serviceIdentifier;
}
public void setAuditComponent(AuditComponent auditComponent)
@@ -58,16 +132,302 @@ public class AuditMethodInterceptor implements MethodInterceptor
this.auditComponent = auditComponent;
}
+ public void setTransactionService(TransactionService transactionService)
+ {
+ this.transactionService = transactionService;
+ }
+
public Object invoke(MethodInvocation mi) throws Throwable
{
- if(disabled)
+ if(!enabled)
{
+ // No auditing
return mi.proceed();
}
+ else if (useNewConfig)
+ {
+ // New configuration to be used
+ return proceed(mi);
+ }
else
{
+ // Use previous configuration
return auditComponent.audit(mi);
}
}
+
+ /**
+ * Allow the given method invocation to proceed, auditing values before invocation and
+ * after returning or throwing.
+ *
+ * @param mi the invocation
+ * @return Returns the method return (if a value is not thrown)
+ * @throws Throwable rethrows any exception generated by the invocation
+ *
+ * @since 3.2
+ */
+ private Object proceed(MethodInvocation mi) throws Throwable
+ {
+ Auditable auditableDef = mi.getMethod().getAnnotation(Auditable.class);
+ if (auditableDef == null)
+ {
+ // No annotation, so just continue as normal
+ return mi.proceed();
+ }
+
+ // First get the argument map, if present
+ Object[] args = mi.getArguments();
+ Map namedArguments = getInvocationArguments(auditableDef, args);
+ // Get the service name
+ String serviceName = publicServiceIdentifier.getPublicServiceName(mi);
+ if (serviceName == null)
+ {
+ // Not a public service
+ return mi.proceed();
+ }
+ String methodName = mi.getMethod().getName();
+
+ // Are we in a nested audit
+ Boolean wasInAudit = inAudit.get();
+ // TODO: Need to make this configurable for the interceptor or a conditional mapping for audit
+ if (wasInAudit != null)
+ {
+ return mi.proceed();
+ }
+ // Record that we have entered an audit method
+ inAudit.set(Boolean.TRUE);
+ try
+ {
+ return proceedWithAudit(mi, auditableDef, serviceName, methodName, namedArguments);
+ }
+ finally
+ {
+ inAudit.set(wasInAudit);
+ }
+ }
+
+ private Object proceedWithAudit(
+ MethodInvocation mi,
+ Auditable auditableDef,
+ String serviceName,
+ String methodName,
+ Map namedArguments) throws Throwable
+ {
+ try
+ {
+ auditInvocationBefore(serviceName, methodName, namedArguments);
+ }
+ catch (Throwable e)
+ {
+ // Failure to audit should not break the invocation
+ logger.error(
+ "Failed to audit pre-invocation: \n" +
+ " Invocation: " + mi,
+ e);
+ }
+
+ // Execute the call
+ Object ret = null;
+ Throwable thrown = null;
+ try
+ {
+ ret = mi.proceed();
+ }
+ catch (Throwable e)
+ {
+ thrown = e;
+ }
+
+ // We don't ALWAYS want to record the return value
+ Object auditRet = auditableDef.recordReturnedObject() ? ret : null;
+ try
+ {
+ auditInvocationAfter(serviceName, methodName, namedArguments, auditRet, thrown);
+ }
+ catch (Throwable e)
+ {
+ // Failure to audit should not break the invocation
+ logger.error(
+ "Failed to audit post-invocation: \n" +
+ " Invocation: " + mi,
+ e);
+ }
+
+ // Done
+ if (thrown != null)
+ {
+ throw thrown;
+ }
+ else
+ {
+ return ret;
+ }
+ }
+
+ /**
+ * @return Returns the arguments mapped by name
+ *
+ * @since 3.2
+ */
+ private Map getInvocationArguments(Auditable auditableDef, Object[] args)
+ {
+ // Use the annotation to name the arguments
+ String[] params = auditableDef.parameters();
+ boolean[] recordable = auditableDef.recordable();
+
+ Map namedArgs = new HashMap(args.length * 2);
+ for (int i = 0; i < args.length; i++)
+ {
+ if (i >= params.length)
+ {
+ // The name list is finished. Unnamed arguments are not recorded.
+ break;
+ }
+ if (i < recordable.length)
+ {
+ // Arguments are recordable by default
+ if (!recordable[i])
+ {
+ // Don't record the argument
+ continue;
+ }
+ }
+ Serializable arg;
+ if (args[i] == null)
+ {
+ arg = null;
+ }
+ else if (args[i] instanceof Serializable)
+ {
+ arg = (Serializable) args[i];
+ }
+ else
+ {
+ // TODO: How to treat non-serializable args
+ // arg = args[i].toString();
+ try
+ {
+ arg = DefaultTypeConverter.INSTANCE.convert(String.class, args[i]);
+ }
+ catch (TypeConversionException e)
+ {
+ // No viable conversion
+ continue;
+ }
+ }
+ // It is named and recordable
+ namedArgs.put(params[i], arg);
+ }
+ // Done
+ return namedArgs;
+ }
+
+ /**
+ * Audit values before the invocation
+ *
+ * @param serviceName the service name
+ * @param methodName the method name
+ * @param namedArguments the named arguments passed to the invocation
+ *
+ * @since 3.2
+ */
+ private void auditInvocationBefore(
+ final String serviceName,
+ final String methodName,
+ final Map namedArguments)
+ {
+ final String rootPath = AuditApplication.buildPath(AUDIT_PATH_API_PRE, serviceName, methodName, AUDIT_SNIPPET_ARGS);
+
+ // Audit in a read-write txn
+ Map auditedData = auditComponent.recordAuditValues(rootPath, namedArguments);
+ // Done
+ if (logger.isDebugEnabled() && auditedData.size() > 0)
+ {
+ logger.debug(
+ "Audited before invocation: \n" +
+ " Values: " + auditedData);
+ }
+ }
+
+ /**
+ * Audit values after the invocation
+ *
+ * @param serviceName the service name
+ * @param methodName the method name
+ * @param namedArguments the named arguments passed to the invocation
+ * @param ret the result of the execution (may be null)
+ * @param thrown the error thrown by the invocation (may be null)
+ *
+ * @since 3.2
+ */
+ private void auditInvocationAfter(
+ String serviceName, String methodName, Map namedArguments,
+ Object ret, Throwable thrown)
+ {
+ final String rootPath = AuditApplication.buildPath(AUDIT_PATH_API_POST, serviceName, methodName);
+
+ final Map auditData = new HashMap(23);
+ 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)
+ {
+ if (ret instanceof Serializable)
+ {
+ auditData.put(AUDIT_SNIPPET_RESULT, (Serializable) ret);
+ }
+ else
+ {
+ // TODO: How do we treat non-serializable return values
+ try
+ {
+ ret = DefaultTypeConverter.INSTANCE.convert(String.class, ret);
+ auditData.put(AUDIT_SNIPPET_RESULT, (String) ret);
+ }
+ catch (TypeConversionException e)
+ {
+ // No viable conversion
+ }
+ }
+ }
+ Map auditedData;
+ if (thrown != null)
+ {
+ StringBuilder sb = new StringBuilder(1024);
+ StackTraceUtil.buildStackTrace(
+ thrown.getMessage(), thrown.getStackTrace(), sb, Integer.MAX_VALUE);
+ auditData.put(AUDIT_SNIPPET_ERROR, sb.toString());
+
+ // An exception will generally roll the current transaction back
+ RetryingTransactionCallback