/* * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Thierry Delprat */ package org.nuxeo.ecm.platform.audit.service; import static org.nuxeo.ecm.core.schema.FacetNames.SYSTEM_DOCUMENT; import java.io.Serializable; import java.security.Principal; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import java.util.Set; import javax.el.ELException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jboss.el.ExpressionFactoryImpl; import org.nuxeo.ecm.core.api.CoreInstance; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.DocumentNotFoundException; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.LifeCycleConstants; import org.nuxeo.ecm.core.api.NuxeoPrincipal; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.PropertyException; import org.nuxeo.ecm.core.api.event.DocumentEventTypes; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.event.DeletedDocumentModel; import org.nuxeo.ecm.core.event.Event; import org.nuxeo.ecm.core.event.EventBundle; import org.nuxeo.ecm.core.event.EventContext; import org.nuxeo.ecm.core.event.impl.DocumentEventContext; import org.nuxeo.ecm.platform.audit.api.ExtendedInfo; import org.nuxeo.ecm.platform.audit.api.FilterMapEntry; import org.nuxeo.ecm.platform.audit.api.LogEntry; import org.nuxeo.ecm.platform.audit.impl.LogEntryImpl; import org.nuxeo.ecm.platform.audit.service.extension.AdapterDescriptor; import org.nuxeo.ecm.platform.audit.service.extension.AuditBackendDescriptor; import org.nuxeo.ecm.platform.audit.service.extension.ExtendedInfoDescriptor; import org.nuxeo.ecm.platform.el.ExpressionContext; import org.nuxeo.ecm.platform.el.ExpressionEvaluator; /** * Abstract class to share code between {@link AuditBackend} implementations * * @author tiry */ public abstract class AbstractAuditBackend implements AuditBackend { protected static final Log log = LogFactory.getLog(AbstractAuditBackend.class); public static final String FORCE_AUDIT_FACET = "ForceAudit"; protected final NXAuditEventsService component; protected final AuditBackendDescriptor config; protected AbstractAuditBackend(NXAuditEventsService component, AuditBackendDescriptor config) { this.component = component; this.config = config; } protected final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(new ExpressionFactoryImpl()); protected DocumentModel guardedDocument(CoreSession session, DocumentRef reference) { if (session == null) { return null; } if (reference == null) { return null; } try { return session.getDocument(reference); } catch (DocumentNotFoundException e) { return null; } } protected DocumentModelList guardedDocumentChildren(CoreSession session, DocumentRef reference) { return session.getChildren(reference); } protected LogEntry doCreateAndFillEntryFromDocument(DocumentModel doc, Principal principal) { LogEntry entry = newLogEntry(); entry.setDocPath(doc.getPathAsString()); entry.setDocType(doc.getType()); entry.setDocUUID(doc.getId()); entry.setRepositoryId(doc.getRepositoryName()); entry.setPrincipalName(SecurityConstants.SYSTEM_USERNAME); entry.setCategory("eventDocumentCategory"); entry.setEventId(DocumentEventTypes.DOCUMENT_CREATED); // why hard-code it if we have the document life cycle? entry.setDocLifeCycle("project"); Calendar creationDate = (Calendar) doc.getProperty("dublincore", "created"); if (creationDate != null) { entry.setEventDate(creationDate.getTime()); } doPutExtendedInfos(entry, null, doc, principal); return entry; } protected void doPutExtendedInfos(LogEntry entry, EventContext eventContext, DocumentModel source, Principal principal) { ExpressionContext context = new ExpressionContext(); if (eventContext != null) { expressionEvaluator.bindValue(context, "message", eventContext); } if (source != null) { expressionEvaluator.bindValue(context, "source", source); // inject now the adapters for (AdapterDescriptor ad : component.getDocumentAdapters()) { if (source instanceof DeletedDocumentModel) { continue; // skip } Object adapter = source.getAdapter(ad.getKlass()); if (adapter != null) { expressionEvaluator.bindValue(context, ad.getName(), adapter); } } } if (principal != null) { expressionEvaluator.bindValue(context, "principal", principal); } // Global extended info populateExtendedInfo(entry, source, context, component.getExtendedInfoDescriptors()); // Event id related extended info populateExtendedInfo(entry, source, context, component.getEventExtendedInfoDescriptors().get(entry.getEventId())); if (eventContext != null) { @SuppressWarnings("unchecked") Map<String, Serializable> map = (Map<String, Serializable>) eventContext.getProperty("extendedInfos"); if (map != null) { Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos(); for (Entry<String, Serializable> en : map.entrySet()) { Serializable value = en.getValue(); if (value != null) { extendedInfos.put(en.getKey(), newExtendedInfo(value)); } } } } } /** * @since 7.4 */ protected void populateExtendedInfo(LogEntry entry, DocumentModel source, ExpressionContext context, Collection<ExtendedInfoDescriptor> extInfos) { if (extInfos != null) { Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos(); for (ExtendedInfoDescriptor descriptor : extInfos) { String exp = descriptor.getExpression(); Serializable value = null; try { value = expressionEvaluator.evaluateExpression(context, exp, Serializable.class); } catch (PropertyException | UnsupportedOperationException e) { if (source instanceof DeletedDocumentModel) { log.debug("Can not evaluate the expression: " + exp + " on a DeletedDocumentModel, skipping."); } continue; } catch (ELException e) { continue; } if (value == null) { continue; } extendedInfos.put(descriptor.getKey(), newExtendedInfo(value)); } } } @Override public Set<String> getAuditableEventNames() { return component.getAuditableEventNames(); } protected LogEntry buildEntryFromEvent(Event event) { EventContext ctx = event.getContext(); String eventName = event.getName(); Date eventDate = new Date(event.getTime()); LogEntry entry = newLogEntry(); entry.setEventId(eventName); entry.setEventDate(eventDate); if (ctx instanceof DocumentEventContext) { DocumentEventContext docCtx = (DocumentEventContext) ctx; DocumentModel document = docCtx.getSourceDocument(); if (document.hasFacet(SYSTEM_DOCUMENT) && !document.hasFacet(FORCE_AUDIT_FACET)) { // do not log event on System documents // unless it has the FORCE_AUDIT_FACET facet return null; } Boolean disabled = (Boolean) docCtx.getProperty(NXAuditEventsService.DISABLE_AUDIT_LOGGER); if (disabled != null && disabled) { // don't log events with this flag return null; } Principal principal = docCtx.getPrincipal(); Map<String, Serializable> properties = docCtx.getProperties(); if (document != null) { entry.setDocUUID(document.getId()); entry.setDocPath(document.getPathAsString()); entry.setDocType(document.getType()); entry.setRepositoryId(document.getRepositoryName()); } if (principal != null) { String principalName = null; if (principal instanceof NuxeoPrincipal) { principalName = ((NuxeoPrincipal) principal).getActingUser(); } else { principalName = principal.getName(); } entry.setPrincipalName(principalName); } else { log.warn("received event " + eventName + " with null principal"); } entry.setComment((String) properties.get("comment")); if (document instanceof DeletedDocumentModel) { entry.setComment("Document does not exist anymore!"); } else { if (document.isLifeCycleLoaded()) { entry.setDocLifeCycle(document.getCurrentLifeCycleState()); } } if (LifeCycleConstants.TRANSITION_EVENT.equals(eventName)) { entry.setDocLifeCycle((String) docCtx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TO)); } String category = (String) properties.get("category"); if (category != null) { entry.setCategory(category); } else { entry.setCategory("eventDocumentCategory"); } doPutExtendedInfos(entry, docCtx, document, principal); } else { Principal principal = ctx.getPrincipal(); Map<String, Serializable> properties = ctx.getProperties(); if (principal != null) { String principalName; if (principal instanceof NuxeoPrincipal) { principalName = ((NuxeoPrincipal) principal).getActingUser(); } else { principalName = principal.getName(); } entry.setPrincipalName(principalName); } entry.setComment((String) properties.get("comment")); String category = (String) properties.get("category"); entry.setCategory(category); doPutExtendedInfos(entry, ctx, null, principal); } return entry; } @Override public List<LogEntry> queryLogsByPage(String[] eventIds, String dateRange, String category, String path, int pageNb, int pageSize) { String[] categories = { category }; return queryLogsByPage(eventIds, dateRange, categories, path, pageNb, pageSize); } @Override public List<LogEntry> queryLogsByPage(String[] eventIds, Date limit, String category, String path, int pageNb, int pageSize) { String[] categories = { category }; return queryLogsByPage(eventIds, limit, categories, path, pageNb, pageSize); } @Override public LogEntry newLogEntry() { return new LogEntryImpl(); } @Override public abstract ExtendedInfo newExtendedInfo(Serializable value); protected long syncLogCreationEntries(BaseLogEntryProvider provider, String repoId, String path, Boolean recurs) { provider.removeEntries(DocumentEventTypes.DOCUMENT_CREATED, path); try (CoreSession session = CoreInstance.openCoreSession(repoId)) { DocumentRef rootRef = new PathRef(path); DocumentModel root = guardedDocument(session, rootRef); long nbAddedEntries = doSyncNode(provider, session, root, recurs); if (log.isDebugEnabled()) { log.debug("synced " + nbAddedEntries + " entries on " + path); } return nbAddedEntries; } } protected long doSyncNode(BaseLogEntryProvider provider, CoreSession session, DocumentModel node, boolean recurs) { long nbSyncedEntries = 1; Principal principal = session.getPrincipal(); List<DocumentModel> folderishChildren = new ArrayList<DocumentModel>(); provider.addLogEntry(doCreateAndFillEntryFromDocument(node, session.getPrincipal())); for (DocumentModel child : guardedDocumentChildren(session, node.getRef())) { if (child.isFolder() && recurs) { folderishChildren.add(child); } else { provider.addLogEntry(doCreateAndFillEntryFromDocument(child, principal)); nbSyncedEntries += 1; } } if (recurs) { for (DocumentModel folderChild : folderishChildren) { nbSyncedEntries += doSyncNode(provider, session, folderChild, recurs); } } return nbSyncedEntries; } @Override public void logEvents(EventBundle bundle) { if (!isAuditable(bundle)) { return; } for (Event event : bundle) { logEvent(event); } } protected boolean isAuditable(EventBundle eventBundle) { for (String name : getAuditableEventNames()) { if (eventBundle.containsEventName(name)) { return true; } } return false; } @Override public void logEvent(Event event) { if (!getAuditableEventNames().contains(event.getName())) { return; } LogEntry entry = buildEntryFromEvent(event); if (entry == null) { return; } component.bulker.offer(entry); } @Override public boolean await(long time, TimeUnit unit) throws InterruptedException { return component.bulker.await(time, unit); } /** * Returns the logs given a doc uuid and a repository id. * * @param uuid the document uuid * @param repositoryId the repository id * @return a list of log entries * @since 8.4 */ @Override public List<LogEntry> getLogEntriesFor(String uuid, String repositoryId) { return getLogEntriesFor(uuid, repositoryId); } /** * Returns the logs given a doc uuid. * * @param uuid the document uuid * @return a list of log entries * @deprecated since 8.4, use * {@link (org.nuxeo.ecm.platform.audit.service.AbstractAuditBackend.getLogEntriesFor(String, String))} * instead. */ @Deprecated @Override public List<LogEntry> getLogEntriesFor(String uuid) { return getLogEntriesFor(uuid, Collections.<String, FilterMapEntry> emptyMap(), false); } @Override public List<?> nativeQuery(String query, int pageNb, int pageSize) { return nativeQuery(query, Collections.<String, Object> emptyMap(), pageNb, pageSize); } @Override public List<LogEntry> queryLogs(final String[] eventIds, final String dateRange) { return queryLogsByPage(eventIds, (String) null, (String[]) null, null, 0, 10000); } @Override public List<LogEntry> nativeQueryLogs(final String whereClause, final int pageNb, final int pageSize) { List<LogEntry> entries = new LinkedList<>(); for (Object entry : nativeQuery(whereClause, pageNb, pageSize)) { if (entry instanceof LogEntry) { entries.add((LogEntry) entry); } } return entries; } }