Sunday, March 29, 2015

Alfresco - accessing a specific node's audit trail

Alfresco auditing functionality is not enabled out-of-the-box, you'll have to enable it first.  There's also several auditing OOB webscripts available, including this one, where you get audit info of the last X audits in the system.

PROBLEM:
There is no alfresco oob webscript that grabs the audit trail by documentId/nodeReference. So we will roll our own modelling it off of how the Alfresco Explorer "Preview in Template" show_audit.ftl backend code works. 

HIGH-LEVEL SOLUTION:
The show_audit.ftl is eventually associated to TemplateNode.java, which eventually calls AuditServiceImpl.auditQuery().... to get the audits for that node. The TemplateNode.getAuditTrail() is first driven off of the PATH of the node in question (retrieved via the nodeService.getPath function). This retrieves the path up to the filename, which we can now access the document by node reference.  

Now that we figured this out, we essentially pull everything out of the AuditServiceImpl.auditQuery() class method.  We'll copy this code into a new java-backed webscript, passing in the nodeReference as a webscript parameter, and you're off and running!!!


DETAILED SOLUTION:

We'll call the custom java-backed webscript RetrieveAuditTrailForNodeWebscript.java.

package com.cherryshoe.webscript;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.service.cmr.audit.AuditQueryParameters;
import org.alfresco.service.cmr.audit.AuditService;
import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.ISO9075;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.DeclarativeWebScript;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptRequest;

/*
 * This webscript retrieves audit trail info that is modeled off of the explorer show_audit.ftl backend code (TemplateNode.getAuditTrail)
 */
public class RetrieveAuditTrailForNodeWebscript extends DeclarativeWebScript {
    private static Log logger = LogFactory.getLog(RetrieveAuditTrailService.class);

    private NodeService nodeService;
    private NamespaceService namespaceService;
    private AuditService auditService;
    private Repository repository;
    
    @Override
    protected Map<String, Object> executeImpl(WebScriptRequest req,
            Status status, Cache cache) {
        Map<String, Object> model = new HashMap<String, Object>();
        
        try {    
            // check if all required properties filled out
            String nodeRefString = req.getParameter("nodeRef");
            if(StringUtils.isBlank(nodeRefString)) throw new Exception("property 'nodeRef' missing when processing request");

            // strip off the :/ so workspace://SpacesStore/9d1095ca-214a-4f48-a162-b1452c566556 looks like workspace/SpacesStore/9d1095ca-214a-4f48-a162-b1452c566556
            nodeRefString = nodeRefString.replace(":/", "");
            NodeRef nodeRef = repository.findNodeRef("node", nodeRefString.split("/"));
            List<TemplateAuditInfo> auditTrailList = retrieveAuditTrail(nodeRef);
    
            // convert list to how we want it displayed
            String applicationName = "";
            List<Map<String, Object>> forDisplayList = new ArrayList<Map<String, Object>>(auditTrailList.size());
            for (TemplateAuditInfo tai: auditTrailList) {
                if (logger.isDebugEnabled()) {
                    logger.debug(tai.toString());
                }
                Map<String, Object> mapToAdd = new HashMap<String, Object>();
                mapToAdd.put("id", tai.getId());
                mapToAdd.put("userName", tai.getUserIdentifier());
                mapToAdd.put("applicationMethod", tai.getAuditMethod());
                mapToAdd.put("date", tai.getDate());
                forDisplayList.add(mapToAdd);
                if (StringUtils.isBlank(applicationName)) applicationName = tai.getAuditApplication();
            }
            
            Map<QName, Serializable> props = nodeService.getProperties(nodeRef);
            String fileName = (String)props.get(ContentModel.PROP_NAME);
                                
            model.put("data", forDisplayList);
            model.put("applicationName", applicationName);
            model.put("nodeRef", nodeRef.getId());
            model.put("fileName", fileName);
            model.put("count", auditTrailList.size());
            model.put("returnStatus", Boolean.TRUE);
            model.put("statusMessage", "Successfully retrieved audit trail for nodeRef[" + nodeRef.getId() + "]");
            
        } catch (Exception e) {
            logger.warn(e.getMessage());
            model.put("returnStatus", Boolean.FALSE);
            model.put("statusMessage", e.getMessage());
        }
        return model;
    }

    protected List<TemplateAuditInfo> retrieveAuditTrail(NodeRef nodeRef) {
        final List<TemplateAuditInfo> result = new ArrayList<TemplateAuditInfo>();
        
        // create the callback for auditQuery method
        final AuditQueryCallback callback = new AuditQueryCallback()
        {
            public boolean valuesRequired()
            {
                return true;
            }

            public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error)
            {
                throw new AlfrescoRuntimeException("Failed to retrieve audit data.", error);
            }

            public boolean handleAuditEntry(Long entryId, String applicationName, String user, long time,
                    Map<String, Serializable> values)
            {
                TemplateAuditInfo auditInfo = new TemplateAuditInfo(entryId, applicationName, user, time, values);
                result.add(auditInfo);
                return true;
            }
        };

        // resolve the path of the node 
        final String nodePath = ISO9075.decode(nodeService.getPath(nodeRef).toPrefixString(namespaceService)); 

        // run as admin user to allow everyone to see audit information
        // (new 3.4 API doesn't allow this by default)
        AuthenticationUtil.runAs(new RunAsWork<Object>()
        {
            public Object doWork() throws Exception
            {
                String applicationName = "alfresco-access";
                AuditQueryParameters pathParams = new AuditQueryParameters();
                pathParams.setApplicationName(applicationName);
                pathParams.addSearchKey("/alfresco-access/transaction/path", nodePath);
                auditService.auditQuery(callback, pathParams, -1);
                
                AuditQueryParameters copyFromPathParams = new AuditQueryParameters();
                copyFromPathParams.setApplicationName(applicationName);
                copyFromPathParams.addSearchKey("/alfresco-access/transaction/copy/from/path", nodePath);
                auditService.auditQuery(callback, copyFromPathParams, -1);
                
                AuditQueryParameters moveFromPathParams = new AuditQueryParameters();
                moveFromPathParams.setApplicationName(applicationName);
                moveFromPathParams.addSearchKey("/alfresco-access/transaction/move/from/path", nodePath);
                auditService.auditQuery(callback, moveFromPathParams, -1);
                return null;
            }
        }, AuthenticationUtil.getAdminUserName());
        
        // sort audit entries by time of generation
        Collections.sort(result, new Comparator<TemplateAuditInfo>()
        {
            public int compare(TemplateAuditInfo o1, TemplateAuditInfo o2)
            {
                return o1.getDate().compareTo(o2.getDate());
            }
        });
        return result;
    }
    
    public class TemplateAuditInfo
    {
        private Long id;
        private String applicationName;
        private String userName;
        private long time;
        private Map<String, Serializable> values;

        public TemplateAuditInfo(Long id, String applicationName, String userName, long time, Map<String, Serializable> values)
        {
            this.id = id;
            this.applicationName = applicationName;
            this.userName = userName;
            this.time = time;
            this.values = values;
        }
        
        public Long getId() {
            return id;
        }
        
        public void setId(Long id) {
            this.id = id;
        }
        
        public String getAuditApplication()
        {
            return this.applicationName;
        }

        public String getUserIdentifier()
        {
            return this.userName;
        }

        public Date getDate()
        {
            return new Date(time);
        }

        public String getAuditMethod()
        {
            return this.values.get("/alfresco-access/transaction/action").toString();
        }
        
        public Map<String, Serializable> getValues()
        {
            return this.values;
        }

        @Override
        public String toString() {
            return "TemplateAuditInfo [id=" + id + ", applicationName="
                    + applicationName + ", userName=" + userName + ", time="
                    + time + ", values=" + values + "]";
        }      
        
    }

    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    public void setNamespaceService(NamespaceService namespaceService) {
        this.namespaceService = namespaceService;
    }

    public void setAuditService(AuditService auditService) {
        this.auditService = auditService;
    }

    public void setRepository(Repository repository) {
        this.repository = repository;
    }

    
}

2 comments:

  1. Thanks Judy, I was about to do this same thing but since you have beat me to it, I will simply steal your code and modify it. - James E.

    ReplyDelete

I appreciate your time in leaving a comment!