David Caruana 575c970565 Merging BRANCHES/DEV/CMIS_10 to HEAD:
17717: This check-in contains changes in Java and .NET TCK tests related to CMIS-43  and CMIS-44 JIRA tasks. Also some bugs were faced out and fixed in 
   17727: CMIS-69: Alfresco to CMIS ACL mapping: Part 1: API
   17732: Merge HEAD to DEV/CMIS10
   17756: MOB-563: SQL Tests - Lexer
   17764: CMIS-69: Alfresco to CMIS ACL mapping: get ACL support
   17802: More for CMIS-69: Alfresco to CMIS ACL mapping. Implementation for applyAcl.
   17830: Fixes for CMIS lexer and parser tests
   17838: Access fix ups for access by the WS/Rest layers
   17869: 1) remote-api:
   17874: SAIL-146: Alfresco to CMIS ACL mapping: Support to group ACEs by principal id
   17883: Adjust version properties for dev/cmis10 branch.
   17885: Update OASIS CMIS TC status.
   17889: Fix issue where objectid is not rendered correctly for CMIS private working copies.
   17890: SAIL-146: Alfresco to CMIS ACL mapping: Fixes for ACL merging when reporting and ordering of ACEs. Report full permissions and not unique short names.
   17902: Fix issue where CMIS queries via GET used incorrect defaults for paging.
   17909: Fix CMIS link relations for folder tree.
   17912: Fix CMIS type descendants atompub link
   17922: Update AtomPub binding to CMIS 1.0 CD05 XSDs.
   17924: SAIL-146: Alfresco to CMIS ACL mapping: Test set using full permissions (as opposed to short unique names)
   17927: Fix content stream create/update status to comply with CMIS 1.0 CD05.
   17934: Resolve encoding issues in CMIS AtomPub binding.
   17973: SAIL-171: CMIS Renditions REST binding
   17975: SAIL-146: Alfresco to CMIS ACL mapping: Completed AllowedAction and Permissions mapping. Added missing canDeleteTree.
   17990: Update CMIS AtomPub to CD06
   17996: Updates for cmis.alfresco.com for CD06 in prep for public review 2.
   18007: WS-Bindings were updated with CMIS 1.0 cd06 changes.
   18016: CMIS web services: Add missing generated files from WSDL
   18018: CMIS index page updates for cmis.alfresco.com
   18041: Merged HEAD to DEV/CMIS_10
   18059: SAIL-227:
   18067: SAIL-157: Strict vs Non-Strict Query Language: Enforce restrictions on the use of SCORE() and CONTAINS()
   18080: Fix for SAIL-213:Bug: Query engine does not check that select list properties are valid for selectors
   18131: SAIL-156: Query Language Compliance: Fix support for LIKE, including escaping of '%' and '_' with '\'.
   18132: SAIL-156: Query Language Compliance: Fix support for LIKE, including escaping of '%' and '_' with '\': Fix underlying lucene impl for prefix and fuzzy queries to match wildcard/like
   18143: SAIL-156: Query Language Compliance: Fix and check qualifiers in IN_TREE and IN_FOLDER. Improved scoring for CONTAINS()
   18173: SAIL-245: Exclude thumbnails from normal query results
   18179: SAIL 214: Query Language Compliance: Check for valid object ids in IN_FOLDER and IN_TREE
   18210: SAIL-156:  Query Language Compliance: Support for simple column aliases in predicates/function arguments/embedded FTS. Check property/selector binding in embedded FTS.
   18211: SAIL-156:  Query Language Compliance: Support for simple column aliases in predicates/function arguments/embedded FTS. Check property/selector binding in embedded FTS.
   18215: SAIL 156: Query Language Compliance: Fix CMIS type info to reflect the underlying settings of the Alfresco type for includeInSuperTypeQuery
   18244: SAIL 156: Query Language Compliance: includeInSuperTypeQuery -> includedInSuperTypeQuery: First cut of cmis query test model. Fixed modelSchema.xml to validate
   18255: SAIL 156: Query Language Compliance: First set of tests for predicates using properties mapped to CMIS Strings.
   18261: CMIS-49 SAIL-163: Alfresco to CMIS Change Log mapping - New CMIS Audit mapping is implemented. ChangeLogDataExtractor was added.
   18263: Build Fix
   18285: SAIL 156: Query Language Compliance: Restrictions on predicates that may be used by single-valued and multi-valued properties
   18287: SAIL-186: Changes to make CMIS Rendition REST bindings pass new TCK tests
   18291: Fix Eclipse classpath problems
   18323: CMIS-44 SAIL-187: Change Log tests (WS) – Java and .NET tests for change log were implemented.
   18325: SAIL 156: Query Language Compliance: Fixes and tests for d:mltext mappings
   18329: Updated Chemistry TCK jar including Dave W's rendition tests.
   18333: Fix compile error - spurious imports.
   18334: Fix issue where absurl web script method failed when deployed to root context.
   18339: Update CMIS index page for start of public review 2.
   18387: SAIL-147: CMIS ACL REST bindings + framework fixes
   18392: Fix typo
   18394: SAIL 156: Query Language Compliance: Fixes and tests for d:<numeric>
   18406: SAIL 156: Query Language Compliance: Remaining type/predicate combinations. Restriction of In/Comparisons for ID/Boolean
   18408: CMIS Query language - remove (pointless) multi-valued column from language definition
   18409: Formatting change for CMIS.g
   18410: Formatting change for FTS.g
   18411: CMIS TCK tests were updated to CMIS 1.0 cd06 schemas.
   18412: SAIL 156: Query Language Compliance: Tests and fixes for aliases for all data types in simple predicates (they behave as the direct column reference)
   18417: Update Chemistry TCK which now incorporates Dave W's ACL tests.
   18419: Update CMIS index page to include public review end date.
   18427: SAIL 156: Query Language Compliance: Expose multi-valued properties in queries. Tests for all accessors. Fix content length to be long.
   18435: SAIL 156: Query Language Compliance: Use queryable correctly and fix up model mappings. Add tests for baseTypeId, contentStreamId and path.
   18472: SAIL 156: Query Language Compliance: Tests and fixes for FTS/Contains expressions. Adhere strictly to the spec - no extensions available by default. Improved FTS error reporting (and stop any recovery).
   18477: SAIL-164: CMIS change log REST bindings
   18495: SAIL 156: Query Language Compliance: Tests and fixes for escaping in string literals, LIKE and FTS expressions.
   18537: SAIL 156: Query Language Compliance: Sorting support. Basic sort test for all orderable/indexed CMIS properties.
   18538: SAIL-164: CMIS change log fixes for TCK compliance
   18547: SAIL 156: Query Language Compliance: Ordering tests for all datatypes, including null values. 
   18582: Incorporate latest Chemistry TCK
   18583: Update list of supported CMIS capabilities in index page.
   18606: SAIL-156, SAIL-157, SAIL-158: Query Language Compliance: Respect all query options including locale. Fixes and tests for MLText cross language support.
   18608: SAIL-159: Java / Javascript API access to CMIS Query Language
   18617: SAIL-158: Query Tests: Check policy and relationship types are not queryable.
   18636: SAIL-184: ACL tests (WS) 
   18663: ACL tests were updated in accordance with last requirements by David Caruana.
   18680: Update to CMIS CD07
   18681: Fix CMIS ContentStreamId property when document has no content.
   18700: CMIS: Head merge problem resolution.

Phase 1: Merge up to and including revision 18700, as this the point where both AtomPub and Web Services TCK tests succeed completely on dev branch.

Note: includes CMIS rendition support ready for integration and testing with DM renditions.

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@18790 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2010-02-23 17:23:42 +00:00

1468 lines
55 KiB
Java

/*
* Copyright (C) 2005-2010 Alfresco Software Limited.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception. You should have received a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing"
*/
package org.alfresco.repo.audit;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
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.AuditEntry;
import org.alfresco.repo.audit.model.AuditModelRegistry;
import org.alfresco.repo.audit.model.TrueFalseUnset;
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.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.Auditable;
import org.alfresco.service.NotAuditable;
import org.alfresco.service.PublicService;
import org.alfresco.service.cmr.audit.AuditInfo;
import org.alfresco.service.cmr.audit.AuditQueryParameters;
import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.PathMapper;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ReflectiveMethodInvocation;
import org.springframework.extensions.surf.util.ParameterCheck;
/**
* The default audit component implementation. TODO: Implement before, after and exception filtering. At the moment
* these filters are ignored. TODO: Respect audit internal - at the moment audit internal is fixed to false.
* <p/>
* The V3.2 audit functionality is contained within the same component. When the newer audit
* implementation has been tested and approved, then older ones will be deprecated as necessary.
*
* @author Andy Hind
* @author Derek Hulley
*/
public class AuditComponentImpl implements AuditComponent
{
/**
* The application name to use for audit entries generated by method interception around public services.
*/
private static final String SYSTEM_APPLICATION = "SystemMethodInterceptor";
/**
* Logging
*/
private static Log logger = LogFactory.getLog(AuditComponentImpl.class);
/**
* Suspend resume auditing
*/
private static ThreadLocal<Boolean> auditFlag = new ThreadLocal<Boolean>();
/**
* IOC
*/
private PublicServiceIdentifier publicServiceIdentifier;
private AuditConfiguration auditConfiguration;
private AuditDAO auditDAO;
private TransactionService transactionService;
private NodeService nodeService;
private NamespacePrefixResolver namespacePrefixResolver;
private AuditModel auditModel;
/**
* Keep hold of the host where the audit occurs. TODO: Check that we get the correct address ...
*/
private InetAddress auditHost;
/**
* Default constructor
*/
public AuditComponentImpl()
{
super();
// Initialise the host address
try
{
auditHost = InetAddress.getLocalHost();
}
catch (UnknownHostException e)
{
logger.error("Failed to get local host address", e);
}
}
/*
* IOC property setters
*/
/**
* Set the DAO for recording auditable information when no exception occurs.
*/
public void setAuditDAO(AuditDAO auditDAO)
{
this.auditDAO = auditDAO;
}
/**
* Set the DAO for recording failed actions - this is done in another transaction.
*/
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
/**
* Set the NodeService for path extracting.
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* Set the audit configuration.
*/
public void setAuditConfiguration(AuditConfiguration auditConfiguration)
{
this.auditConfiguration = auditConfiguration;
}
/**
* Set the helper used to identify public services
*/
public void setPublicServiceIdentifier(PublicServiceIdentifier publicServiceIdentifier)
{
this.publicServiceIdentifier = publicServiceIdentifier;
}
/**
* Set the audit model.
*/
public void setAuditModel(AuditModel auditModel)
{
this.auditModel = auditModel;
}
/**
* Set the namespacePrefixResolver.
*/
public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver)
{
this.namespacePrefixResolver = namespacePrefixResolver;
}
public Object audit(MethodInvocation mi) throws Throwable
{
if ((auditFlag.get() == null) || (!auditFlag.get().booleanValue()))
{
if (auditModel instanceof AuditEntry && ((AuditEntry) auditModel).getEnabled() == TrueFalseUnset.TRUE)
{
boolean auditInternal = (auditModel.getAuditInternalServiceMethods(mi) == TrueFalseUnset.TRUE);
try
{
Method method = mi.getMethod();
String methodName = method.getName();
String serviceName = publicServiceIdentifier.getPublicServiceName(mi);
if (!auditInternal)
{
auditFlag.set(Boolean.TRUE);
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("Auditing internal service use for - " + serviceName + "." + methodName);
}
}
if (method.isAnnotationPresent(Auditable.class))
{
if (serviceName != null)
{
if (logger.isDebugEnabled())
{
logger.debug("Auditing - " + serviceName + "." + methodName);
}
return auditImpl(mi, true);
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("UnknownService." + methodName);
}
return auditImpl(mi, true);
}
}
else if (method.isAnnotationPresent(NotAuditable.class))
{
if (logger.isDebugEnabled())
{
logger.debug("Not Audited. " + serviceName + "." + methodName);
}
return mi.proceed();
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("Unannotated service method " + serviceName + "." + methodName);
}
if (method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAnnotationPresent(PublicService.class))
{
throw new RuntimeException("Unannotated service method " + serviceName + "." + methodName);
}
else
{
return mi.proceed();
}
}
}
finally
{
if (!auditInternal)
{
auditFlag.set(Boolean.FALSE);
}
}
}
else
{
return mi.proceed();
}
}
else
{
return mi.proceed();
}
}
/**
* Internal audit of a method invocation
*
* @param mi -
* the method to audit
* @return - the return object from the audited method
* @throws Throwable -
* any Throwable that can be thrown by th audtied method.
*/
public Object auditImpl(MethodInvocation mi, boolean execute) throws Throwable
{
final AuditState auditInfo = new AuditState(auditConfiguration);
// RecordOptions recordOptions = auditModel.getAuditRecordOptions(mi);
AuditMode auditMode = AuditMode.UNSET;
try
{
Object o = null;
auditMode = beforeInvocation(auditMode, auditInfo, mi);
if (execute)
{
o = mi.proceed();
auditMode = postInvocation(auditMode, auditInfo, mi, o);
}
if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.SUCCESS))
{
RetryingTransactionCallback<Object> cb = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
auditDAO.audit(auditInfo);
return null;
}
};
boolean requiresNew = (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_READ_WRITE);
transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, requiresNew);
}
return o;
}
catch (Throwable t)
{
auditMode = onError(auditMode, auditInfo, mi, t);
if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.FAIL))
{
try
{
RetryingTransactionCallback<Object> cb = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
auditDAO.audit(auditInfo);
return null;
}
};
transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, true);
}
catch (Throwable tt)
{
throw new AuditException("Failed to audit exception", new Object[] { tt }, t);
}
}
throw t;
}
}
/**
* Helper method to set auditable properties and to determine if auditing is required when an exception is caught in
* the audited method.
*
* @param auditMode
* @param auditInfo
* @param mi
* @param t
* @return - the audit mode
*/
private AuditMode onError(AuditMode auditMode, AuditState auditInfo, MethodInvocation mi, Throwable t)
{
if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.FAIL))
{
auditInfo.setFail(true);
auditInfo.setThrowable(t);
}
return auditMode;
}
/**
* Helper method to set audited information after method invocation and to determine if auditing should take place
* based on the method return value.
*
* @param auditMode
* @param auditInfo
* @param mi
* @param returnObject
* @return - the audit mode.
*/
private AuditMode postInvocation(AuditMode auditMode, AuditState auditInfo, MethodInvocation mi, Object returnObject)
{
if (returnObject == null)
{
auditInfo.setReturnObject(null);
}
else if (returnObject instanceof Serializable)
{
auditInfo.setReturnObject((Serializable) returnObject);
}
else
{
auditInfo.setReturnObject(returnObject.toString());
}
Auditable auditable = mi.getMethod().getAnnotation(Auditable.class);
if (auditable.key() == Auditable.Key.RETURN)
{
if (returnObject != null)
{
if (returnObject instanceof NodeRef)
{
NodeRef key = (NodeRef) returnObject;
auditInfo.setKeyStore(key.getStoreRef());
auditInfo.setKeyGUID(key.getId());
RecordOptions recordOptions = auditModel.getAuditRecordOptions(mi);
if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE)
{
auditInfo.setPath(getNodePath(key));
}
}
else if (returnObject instanceof StoreRef)
{
auditInfo.setKeyStore((StoreRef) returnObject);
}
else if (returnObject instanceof ChildAssociationRef)
{
ChildAssociationRef car = (ChildAssociationRef) returnObject;
auditInfo.setKeyStore(car.getChildRef().getStoreRef());
auditInfo.setKeyGUID(car.getChildRef().getId());
RecordOptions recordOptions = auditModel.getAuditRecordOptions(mi);
if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE)
{
auditInfo.setPath(nodeService.getPath(car.getChildRef()).toString());
}
}
else
{
logger.warn("Key argument is not a node, store or child assoc ref for return object on "
+ publicServiceIdentifier.getPublicServiceName(mi) + "." + mi.getMethod().getName() + " it is " + returnObject.getClass().getName());
}
}
}
// If the user name is not set, try and set it after the method call.
// This covers authentication when the user is only known after the call.
if (auditInfo.getUserIdentifier() == null)
{
auditInfo.setUserIdentifier(AuthenticationUtil.getFullyAuthenticatedUser());
}
return auditMode;
}
/**
* Set auditable information and determine if auditing is required before method invocation. This would normally be
* based on the method arguments.
*
* @param auditMode
* @param auditInfo
* @param mi
* @return - the audit mode.
*/
private AuditMode beforeInvocation(AuditMode auditMode, AuditState auditInfo, MethodInvocation mi)
{
AuditMode effectiveAuditMode = auditModel.beforeExecution(auditMode, mi);
if (auditMode != AuditMode.NONE)
{
String methodName = mi.getMethod().getName();
String serviceName = publicServiceIdentifier.getPublicServiceName(mi);
auditInfo.setAuditApplication(SYSTEM_APPLICATION);
auditInfo.setAuditConfiguration(auditConfiguration);
auditInfo.setAuditMethod(methodName);
auditInfo.setAuditService(serviceName);
auditInfo.setClientAddress(null);
auditInfo.setDate(new Date());
auditInfo.setFail(false);
auditInfo.setFiltered(false);
auditInfo.setHostAddress(auditHost);
auditInfo.setPath(null);
Auditable auditable = mi.getMethod().getAnnotation(Auditable.class);
Object key = null;
switch (auditable.key())
{
case ARG_0:
checkArgLength(mi, methodName, serviceName, 0);
key = mi.getArguments()[0];
break;
case ARG_1:
checkArgLength(mi, methodName, serviceName, 1);
key = mi.getArguments()[1];
break;
case ARG_2:
checkArgLength(mi, methodName, serviceName, 2);
key = mi.getArguments()[2];
break;
case ARG_3:
checkArgLength(mi, methodName, serviceName, 3);
key = mi.getArguments()[3];
break;
case ARG_4:
checkArgLength(mi, methodName, serviceName, 4);
key = mi.getArguments()[4];
break;
case ARG_5:
checkArgLength(mi, methodName, serviceName, 5);
key = mi.getArguments()[5];
break;
case ARG_6:
checkArgLength(mi, methodName, serviceName, 6);
key = mi.getArguments()[6];
break;
case ARG_7:
checkArgLength(mi, methodName, serviceName, 7);
key = mi.getArguments()[7];
break;
case ARG_8:
checkArgLength(mi, methodName, serviceName, 8);
key = mi.getArguments()[8];
break;
case ARG_9:
checkArgLength(mi, methodName, serviceName, 9);
key = mi.getArguments()[9];
break;
case NO_KEY:
default:
break;
}
if (key != null)
{
RecordOptions recordOptions = auditModel.getAuditRecordOptions(mi);
if (key instanceof NodeRef)
{
auditInfo.setKeyStore(((NodeRef) key).getStoreRef());
auditInfo.setKeyGUID(((NodeRef) key).getId());
if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE)
{
auditInfo.setPath(getNodePath((NodeRef) key));
}
}
else if (key instanceof StoreRef)
{
auditInfo.setKeyStore((StoreRef) key);
}
else if (key instanceof ChildAssociationRef)
{
ChildAssociationRef car = (ChildAssociationRef) key;
auditInfo.setKeyStore(car.getParentRef().getStoreRef());
auditInfo.setKeyGUID(car.getParentRef().getId());
if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE)
{
auditInfo.setPath(getNodePath(car.getParentRef()));
}
}
else if (key instanceof SearchParameters)
{
SearchParameters sp = (SearchParameters) key;
if (sp.getStores().size() > 0)
{
auditInfo.setKeyStore(sp.getStores().get(0));
}
}
else
{
logger.warn("Key argument is not a node, store or child assoc reference or search parameters on "
+ serviceName + "." + methodName + " it is " + key.getClass().getName());
}
}
auditInfo.setKeyPropertiesAfter(null);
auditInfo.setKeyPropertiesBefore(null);
auditInfo.setMessage(null);
if (mi.getArguments() != null)
{
Serializable[] serArgs = new Serializable[mi.getArguments().length];
for (int i = 0; i < mi.getArguments().length; i++)
{
if ((auditable.recordable() == null) || (auditable.recordable().length <= i) || auditable.recordable()[i])
{
if (mi.getArguments()[i] == null)
{
serArgs[i] = null;
}
else if (mi.getArguments()[i] instanceof Serializable)
{
serArgs[i] = (Serializable) mi.getArguments()[i];
}
else
{
serArgs[i] = mi.getArguments()[i].toString();
}
}
else
{
serArgs[i] = "********";
}
}
auditInfo.setMethodArguments(serArgs);
}
auditInfo.setReturnObject(null);
auditInfo.setSessionId(null);
auditInfo.setThrowable(null);
auditInfo.setTxId(AlfrescoTransactionSupport.getTransactionId());
auditInfo.setUserIdentifier(AuthenticationUtil.getFullyAuthenticatedUser());
}
return effectiveAuditMode;
}
private void checkArgLength(MethodInvocation mi, String methodName, String serviceName, int position)
{
if (mi.getArguments().length <= position)
{
logger.warn("Auditable annotation on " + serviceName + "." + methodName + " references non existant argument");
}
}
/*
* (non-Javadoc)
*
* @see org.alfresco.repo.audit.AuditComponent#beforeMethodCallManualAudit(org.aopalliance.intercept.MethodInvocation)
*/
@SuppressWarnings("unchecked")
public void beforeMethodCallManualAudit(Class clazz, Object target, String methodName, Object ... args)
{
Class[] argTypes = new Class[args.length];
for(int i = 0; i < args.length; i++)
{
argTypes[i] = args[i].getClass();
}
Method method;
try
{
method = clazz.getMethod(methodName, argTypes);
}
catch (SecurityException e1)
{
return;
}
catch (NoSuchMethodException e1)
{
return;
}
MethodInvocation methodInvocation = new ReflectiveMethodInvocation(null, target, method, args, null, null) {};
if ((auditFlag.get() == null) || (!auditFlag.get().booleanValue()))
{
if (auditModel instanceof AuditEntry && ((AuditEntry) auditModel).getEnabled() == TrueFalseUnset.TRUE)
{
boolean auditInternal = (auditModel.getAuditInternalServiceMethods(methodInvocation) == TrueFalseUnset.TRUE);
try
{
String serviceName = publicServiceIdentifier.getPublicServiceName(methodInvocation);
if (!auditInternal)
{
auditFlag.set(Boolean.TRUE);
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("Auditing internal service use for - " + serviceName + "." + methodName);
}
}
if (method.isAnnotationPresent(Auditable.class))
{
if (serviceName != null)
{
if (logger.isDebugEnabled())
{
logger.debug("Auditing - " + serviceName + "." + methodName);
}
try
{
auditImpl(methodInvocation, false);
}
catch (Throwable e)
{
}
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("UnknownService." + methodName);
}
try
{
auditImpl(methodInvocation, false);
}
catch (Throwable e)
{
}
}
}
else if (method.isAnnotationPresent(NotAuditable.class))
{
if (logger.isDebugEnabled())
{
logger.debug("Not Audited. " + serviceName + "." + methodName);
}
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("Unannotated service method " + serviceName + "." + methodName);
}
if (method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAnnotationPresent(PublicService.class))
{
throw new RuntimeException("Unannotated service method " + serviceName + "." + methodName);
}
}
}
finally
{
if (!auditInternal)
{
auditFlag.set(Boolean.FALSE);
}
}
}
}
}
/**
* A simple audit entry Currently we ignore filtering here.
*/
public void audit(String source, String description, NodeRef key, Object... args)
{
final AuditState auditInfo = new AuditState(auditConfiguration);
AuditMode auditMode = AuditMode.UNSET;
try
{
auditMode = onApplicationAudit(auditMode, auditInfo, source, description, key, args);
if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.SUCCESS))
{
RetryingTransactionCallback<Object> cb = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
auditDAO.audit(auditInfo);
return null;
}
};
transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, false);
}
}
catch (Throwable t)
{
auditMode = onError(auditMode, auditInfo, t, source, description, key, args);
if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.FAIL))
{
try
{
RetryingTransactionCallback<Object> cb = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
auditDAO.audit(auditInfo);
return null;
}
};
transactionService.getRetryingTransactionHelper().doInTransaction(cb, false, true);
}
catch (Throwable tt)
{
throw new AuditException("Failed to audit exception", new Object[] { tt }, t);
}
}
throw new AuditException("Application audit failed", t);
}
}
public List<AuditInfo> getAuditTrail(NodeRef nodeRef)
{
return auditDAO.getAuditTrail(nodeRef);
}
private AuditMode onApplicationAudit(AuditMode auditMode, AuditState auditInfo, String source, String description, NodeRef key, Object... args)
{
AuditMode effectiveAuditMode = auditModel.beforeExecution(auditMode, source, description, key, args);
auditModel.getAuditRecordOptions(source);
if (auditMode != AuditMode.NONE)
{
if (source.equals(SYSTEM_APPLICATION))
{
throw new AuditException("Application audit can not use the reserved identifier " + SYSTEM_APPLICATION);
}
auditInfo.setAuditApplication(source);
auditInfo.setAuditConfiguration(auditConfiguration);
auditInfo.setAuditMethod(null);
auditInfo.setAuditService(null);
auditInfo.setClientAddress(null);
auditInfo.setDate(new Date());
auditInfo.setFail(false);
auditInfo.setFiltered(false);
auditInfo.setHostAddress(auditHost);
auditInfo.setPath(null);
if (key != null)
{
auditInfo.setKeyStore(key.getStoreRef());
auditInfo.setKeyGUID(key.getId());
RecordOptions recordOptions = auditModel.getAuditRecordOptions(source);
if (recordOptions != null && recordOptions.getRecordPath() == TrueFalseUnset.TRUE)
{
auditInfo.setPath(getNodePath(key));
}
}
auditInfo.setKeyPropertiesAfter(null);
auditInfo.setKeyPropertiesBefore(null);
auditInfo.setMessage(description);
if (args != null)
{
Serializable[] serArgs = new Serializable[args.length];
for (int i = 0; i < args.length; i++)
{
if (args[i] == null)
{
serArgs[i] = null;
}
else if (args[i] instanceof Serializable)
{
serArgs[i] = (Serializable) args[i];
}
else
{
serArgs[i] = args[i].toString();
}
}
auditInfo.setMethodArguments(serArgs);
}
auditInfo.setReturnObject(null);
auditInfo.setSessionId(null);
auditInfo.setThrowable(null);
auditInfo.setTxId(AlfrescoTransactionSupport.getTransactionId());
auditInfo.setUserIdentifier(AuthenticationUtil.getFullyAuthenticatedUser());
}
return effectiveAuditMode;
}
private AuditMode onError(AuditMode auditMode, AuditState auditInfo, Throwable t, String source, String description, NodeRef key, Object... args)
{
if ((auditMode == AuditMode.ALL) || (auditMode == AuditMode.FAIL))
{
auditInfo.setFail(true);
auditInfo.setThrowable(t);
}
return auditMode;
}
/**
* Returns human readable path for node.
* To improve performance may return simple toString() method of the path.
*
* @param nodeRef
* @return Human readable path for node
*/
private String getNodePath(NodeRef nodeRef)
{
String result = null;
if (nodeService.exists(nodeRef))
{
Path path = nodeService.getPath(nodeRef);
return path.toPrefixString(namespacePrefixResolver);
}
return result;
}
/*
* V3.2 from here on. Put all fixes to the older audit code before this point, please.
*/
private AuditModelRegistry auditModelRegistry;
private PropertyValueDAO propertyValueDAO;
/**
* Set the registry holding the audit models
* @since 3.2
*/
public void setAuditModelRegistry(AuditModelRegistry auditModelRegistry)
{
this.auditModelRegistry = auditModelRegistry;
}
/**
* Set the DAO for manipulating property values
* @since 3.2
*/
public void setPropertyValueDAO(PropertyValueDAO propertyValueDAO)
{
this.propertyValueDAO = propertyValueDAO;
}
/**
* {@inheritDoc}
* @since 3.2
*/
public void 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;
}
Long applicationId = application.getApplicationId();
auditDAO.deleteAuditEntries(applicationId, fromTime, toTime);
// Done
if (logger.isDebugEnabled())
{
logger.debug("Delete audit entries for " + applicationName + " (" + fromTime + " to " + toTime);
}
}
/**
* @param application the audit application object
* @return Returns a copy of the set of disabled paths associated with the application
*/
@SuppressWarnings("unchecked")
private Set<String> getDisabledPaths(AuditApplication application)
{
try
{
Long disabledPathsId = application.getDisabledPathsId();
Set<String> disabledPaths = (Set<String>) propertyValueDAO.getPropertyById(disabledPathsId);
return new HashSet<String>(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 isSourcePathMapped(String sourcePath)
{
return !auditModelRegistry.getAuditPathMapper().isEmpty();
}
/**
* {@inheritDoc}
* @since 3.2
*/
public boolean isAuditPathEnabled(String applicationName, String path)
{
ParameterCheck.mandatory("applicationName", applicationName);
ParameterCheck.mandatory("path", path);
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;
}
// Check the path against the application
application.checkPath(path);
Set<String> disabledPaths = getDisabledPaths(application);
// Check if there are any entries that match or superced 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);
ParameterCheck.mandatory("path", path);
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;
}
// Check the path against the application
application.checkPath(path);
Long disabledPathsId = application.getDisabledPathsId();
Set<String> disabledPaths = getDisabledPaths(application);
// Remove any paths that start with the given path
boolean changed = false;
Iterator<String> 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);
ParameterCheck.mandatory("path", path);
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;
}
// Check the path against the application
application.checkPath(path);
Long disabledPathsId = application.getDisabledPathsId();
Set<String> 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<String> 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<String, Serializable> recordAuditValues(String rootPath, Map<String, Serializable> values)
{
ParameterCheck.mandatory("rootPath", rootPath);
AuditApplication.checkPathFormat(rootPath);
if (values == null || values.isEmpty() || !isSourcePathMapped(rootPath))
{
return Collections.emptyMap();
}
// Build the key paths using the session root path
Map<String, Serializable> pathedValues = new HashMap<String, Serializable>(values.size() * 2);
for (Map.Entry<String, Serializable> 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<String, Serializable> 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<Map<String, Serializable>> callback =
new RetryingTransactionCallback<Map<String,Serializable>>()
{
public Map<String, Serializable> execute() throws Throwable
{
return recordAuditValuesImpl(mappedValues);
}
};
return transactionService.getRetryingTransactionHelper().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<String, Serializable> recordAuditValuesImpl(Map<String, Serializable> mappedValues)
{
// Group the values by root path
Map<String, Map<String, Serializable>> mappedValuesByRootKey = new HashMap<String, Map<String,Serializable>>();
for (Map.Entry<String, Serializable> entry : mappedValues.entrySet())
{
String path = entry.getKey();
String rootKey = AuditApplication.getRootKey(path);
Map<String, Serializable> rootKeyMappedValues = mappedValuesByRootKey.get(rootKey);
if (rootKeyMappedValues == null)
{
rootKeyMappedValues = new HashMap<String, Serializable>(7);
mappedValuesByRootKey.put(rootKey, rootKeyMappedValues);
}
rootKeyMappedValues.put(path, entry.getValue());
}
Map<String, Serializable> allAuditedValues = new HashMap<String, Serializable>(mappedValues.size()*2+1);
// Now audit for each of the root keys
for (Map.Entry<String, Map<String, Serializable>> entry : mappedValuesByRootKey.entrySet())
{
String rootKey = entry.getKey();
Map<String, Serializable> 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<String> 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<String, Serializable> 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 <b>full paths</b>.
* @return Returns all values as audited
*/
private Map<String, Serializable> audit(
AuditApplication application,
Set<String> disabledPaths,
Map<String, Serializable> 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);
}
// Eliminate any paths that have been disabled
Iterator<String> pathedValuesKeyIterator = values.keySet().iterator();
while(pathedValuesKeyIterator.hasNext())
{
String pathedValueKey = pathedValuesKeyIterator.next();
for (String disabledPath : disabledPaths)
{
if (pathedValueKey.startsWith(disabledPath))
{
// The pathed value is excluded
pathedValuesKeyIterator.remove();
}
}
}
// Check if there is anything left
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();
}
// Generate data
Map<String, DataGenerator> generators = application.getDataGenerators(values.keySet());
Map<String, Serializable> auditData = generateData(generators);
// Now extract values
Map<String, Serializable> extractedData = extractData(application, values);
// 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
entryId = auditDAO.createAuditEntry(applicationId, time, username, auditData);
}
// Done
if (logger.isDebugEnabled())
{
logger.debug(
"New audit entry: \n" +
" Application ID: " + applicationId + "\n" +
" Entry ID: " + entryId + "\n" +
" Values: " + values + "\n" +
" Audit Data: " + auditData);
}
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<String, Serializable> extractData(
AuditApplication application,
Map<String, Serializable> values)
{
Map<String, Serializable> newData = new HashMap<String, Serializable>(values.size() + 5);
for (Map.Entry<String, Serializable> entry : values.entrySet())
{
String path = entry.getKey();
Serializable value = entry.getValue();
// Get the applicable extractor
Map<String, DataExtractor> extractors = application.getDataExtractors(path);
for (Map.Entry<String, DataExtractor> extractorElement : extractors.entrySet())
{
String extractorPath = extractorElement.getKey();
DataExtractor extractor = extractorElement.getValue();
// 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: " + path + "\n" +
" Raw value: " + value + "\n" +
" Extractor: " + extractor,
e);
}
// Add it to the map
newData.put(extractorPath, data);
}
}
// Done
if (logger.isDebugEnabled())
{
logger.debug("Extracted audit data: \n" +
" Application: " + application + "\n" +
" Raw values: " + values + "\n" +
" Extracted: " + newData);
}
return newData;
}
/**
* @param generators the data generators
* @return Returns a map of generated data keyed by full path
*
* @since 3.2
*/
private Map<String, Serializable> generateData(Map<String, DataGenerator> generators)
{
Map<String, Serializable> newData = new HashMap<String, Serializable>(generators.size() + 5);
for (Map.Entry<String, DataGenerator> 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);
}
}