001/*
002 * (C) Copyright 2006-2014 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thierry Delprat
016 */
017package org.nuxeo.ecm.platform.audit.service;
018
019import static org.nuxeo.ecm.core.schema.FacetNames.SYSTEM_DOCUMENT;
020
021import java.io.Serializable;
022import java.security.Principal;
023import java.util.ArrayList;
024import java.util.Calendar;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Date;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Set;
033
034import javax.el.ELException;
035
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.jboss.el.ExpressionFactoryImpl;
039import org.nuxeo.ecm.core.api.CoreInstance;
040import org.nuxeo.ecm.core.api.CoreSession;
041import org.nuxeo.ecm.core.api.DocumentModel;
042import org.nuxeo.ecm.core.api.DocumentModelList;
043import org.nuxeo.ecm.core.api.DocumentNotFoundException;
044import org.nuxeo.ecm.core.api.DocumentRef;
045import org.nuxeo.ecm.core.api.LifeCycleConstants;
046import org.nuxeo.ecm.core.api.NuxeoPrincipal;
047import org.nuxeo.ecm.core.api.PathRef;
048import org.nuxeo.ecm.core.api.PropertyException;
049import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
050import org.nuxeo.ecm.core.api.security.SecurityConstants;
051import org.nuxeo.ecm.core.event.DeletedDocumentModel;
052import org.nuxeo.ecm.core.event.Event;
053import org.nuxeo.ecm.core.event.EventBundle;
054import org.nuxeo.ecm.core.event.EventContext;
055import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
056import org.nuxeo.ecm.platform.audit.api.ExtendedInfo;
057import org.nuxeo.ecm.platform.audit.api.FilterMapEntry;
058import org.nuxeo.ecm.platform.audit.api.LogEntry;
059import org.nuxeo.ecm.platform.audit.impl.LogEntryImpl;
060import org.nuxeo.ecm.platform.audit.service.extension.AdapterDescriptor;
061import org.nuxeo.ecm.platform.audit.service.extension.ExtendedInfoDescriptor;
062import org.nuxeo.ecm.platform.el.ExpressionContext;
063import org.nuxeo.ecm.platform.el.ExpressionEvaluator;
064
065/**
066 * Abstract class to share code between {@link AuditBackend} implementations
067 *
068 * @author tiry
069 */
070public abstract class AbstractAuditBackend implements AuditBackend {
071
072    protected static final Log log = LogFactory.getLog(AbstractAuditBackend.class);
073
074    public static final String FORCE_AUDIT_FACET = "ForceAudit";
075
076    protected NXAuditEventsService component;
077
078    @Override
079    public void activate(NXAuditEventsService component) {
080        this.component = component;
081    }
082
083    protected final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(new ExpressionFactoryImpl());
084
085    protected DocumentModel guardedDocument(CoreSession session, DocumentRef reference) {
086        if (session == null) {
087            return null;
088        }
089        if (reference == null) {
090            return null;
091        }
092        try {
093            return session.getDocument(reference);
094        } catch (DocumentNotFoundException e) {
095            return null;
096        }
097    }
098
099    protected DocumentModelList guardedDocumentChildren(CoreSession session, DocumentRef reference) {
100        return session.getChildren(reference);
101    }
102
103    protected LogEntry doCreateAndFillEntryFromDocument(DocumentModel doc, Principal principal) {
104        LogEntry entry = newLogEntry();
105        entry.setDocPath(doc.getPathAsString());
106        entry.setDocType(doc.getType());
107        entry.setDocUUID(doc.getId());
108        entry.setRepositoryId(doc.getRepositoryName());
109        entry.setPrincipalName(SecurityConstants.SYSTEM_USERNAME);
110        entry.setCategory("eventDocumentCategory");
111        entry.setEventId(DocumentEventTypes.DOCUMENT_CREATED);
112        // why hard-code it if we have the document life cycle?
113        entry.setDocLifeCycle("project");
114        Calendar creationDate = (Calendar) doc.getProperty("dublincore", "created");
115        if (creationDate != null) {
116            entry.setEventDate(creationDate.getTime());
117        }
118
119        doPutExtendedInfos(entry, null, doc, principal);
120
121        return entry;
122    }
123
124    protected void doPutExtendedInfos(LogEntry entry, EventContext eventContext, DocumentModel source,
125            Principal principal) {
126
127        ExpressionContext context = new ExpressionContext();
128        if (eventContext != null) {
129            expressionEvaluator.bindValue(context, "message", eventContext);
130        }
131        if (source != null) {
132            expressionEvaluator.bindValue(context, "source", source);
133            // inject now the adapters
134            for (AdapterDescriptor ad : component.getDocumentAdapters()) {
135                if (source instanceof DeletedDocumentModel) {
136                    continue; // skip
137                }
138                Object adapter = source.getAdapter(ad.getKlass());
139                if (adapter != null) {
140                    expressionEvaluator.bindValue(context, ad.getName(), adapter);
141                }
142            }
143        }
144        if (principal != null) {
145            expressionEvaluator.bindValue(context, "principal", principal);
146        }
147
148        // Global extended info
149        populateExtendedInfo(entry, source, context,  component.getExtendedInfoDescriptors());
150        // Event id related extended info
151        populateExtendedInfo(entry, source, context,  component.getEventExtendedInfoDescriptors().get(entry.getEventId()));
152
153        if (eventContext != null) {
154            @SuppressWarnings("unchecked")
155            Map<String, Serializable> map = (Map<String, Serializable>) eventContext.getProperty("extendedInfos");
156            if (map != null) {
157                Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos();
158                for (Entry<String, Serializable> en : map.entrySet()) {
159                    Serializable value = en.getValue();
160                    if (value != null) {
161                        extendedInfos.put(en.getKey(), newExtendedInfo(value));
162                    }
163                }
164            }
165        }
166    }
167
168    /**
169     * @since 7.4
170     */
171    protected void populateExtendedInfo(LogEntry entry, DocumentModel source, ExpressionContext context,
172            Collection<ExtendedInfoDescriptor> extInfos) {
173        if (extInfos != null) {
174            Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos();
175            for (ExtendedInfoDescriptor descriptor : extInfos) {
176                String exp = descriptor.getExpression();
177                Serializable value = null;
178                try {
179                    value = expressionEvaluator.evaluateExpression(context, exp, Serializable.class);
180                } catch (PropertyException | UnsupportedOperationException e) {
181                    if (source instanceof DeletedDocumentModel) {
182                        log.debug("Can not evaluate the expression: " + exp + " on a DeletedDocumentModel, skipping.");
183                    }
184                    continue;
185                } catch (ELException e) {
186                    continue;
187                }
188                if (value == null) {
189                    continue;
190                }
191                extendedInfos.put(descriptor.getKey(), newExtendedInfo(value));
192            }
193        }
194    }
195
196    @Override
197    public Set<String> getAuditableEventNames() {
198        return component.getAuditableEventNames();
199    }
200
201    protected LogEntry buildEntryFromEvent(Event event) {
202        EventContext ctx = event.getContext();
203        String eventName = event.getName();
204        Date eventDate = new Date(event.getTime());
205
206        if (!getAuditableEventNames().contains(event.getName())) {
207            return null;
208        }
209
210        LogEntry entry = newLogEntry();
211        entry.setEventId(eventName);
212        entry.setEventDate(eventDate);
213
214        if (ctx instanceof DocumentEventContext) {
215            DocumentEventContext docCtx = (DocumentEventContext) ctx;
216            DocumentModel document = docCtx.getSourceDocument();
217            if (document.hasFacet(SYSTEM_DOCUMENT) && !document.hasFacet(FORCE_AUDIT_FACET)) {
218                // do not log event on System documents
219               // unless it has the FORCE_AUDIT_FACET facet
220                return null;
221            }
222
223            Boolean disabled = (Boolean) docCtx.getProperty(NXAuditEventsService.DISABLE_AUDIT_LOGGER);
224            if (disabled != null && disabled) {
225                // don't log events with this flag
226                return null;
227            }
228            Principal principal = docCtx.getPrincipal();
229            Map<String, Serializable> properties = docCtx.getProperties();
230
231            if (document != null) {
232                entry.setDocUUID(document.getId());
233                entry.setDocPath(document.getPathAsString());
234                entry.setDocType(document.getType());
235                entry.setRepositoryId(document.getRepositoryName());
236            }
237            if (principal != null) {
238                String principalName = null;
239                if (principal instanceof NuxeoPrincipal) {
240                    principalName = ((NuxeoPrincipal) principal).getActingUser();
241                }
242                entry.setPrincipalName(principalName);
243            } else {
244                log.warn("received event " + eventName + " with null principal");
245            }
246            entry.setComment((String) properties.get("comment"));
247            if (document instanceof DeletedDocumentModel) {
248                entry.setComment("Document does not exist anymore!");
249            } else {
250                if (document.isLifeCycleLoaded()) {
251                    entry.setDocLifeCycle(document.getCurrentLifeCycleState());
252                }
253            }
254            if (LifeCycleConstants.TRANSITION_EVENT.equals(eventName)) {
255                entry.setDocLifeCycle((String) docCtx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TO));
256            }
257            String category = (String) properties.get("category");
258            if (category != null) {
259                entry.setCategory(category);
260            } else {
261                entry.setCategory("eventDocumentCategory");
262            }
263
264            doPutExtendedInfos(entry, docCtx, document, principal);
265
266        } else {
267            Principal principal = ctx.getPrincipal();
268            Map<String, Serializable> properties = ctx.getProperties();
269
270            if (principal != null) {
271                String principalName = null;
272                if (principal instanceof NuxeoPrincipal) {
273                    principalName = ((NuxeoPrincipal) principal).getActingUser();
274                }
275                entry.setPrincipalName(principalName);
276            }
277            entry.setComment((String) properties.get("comment"));
278
279            String category = (String) properties.get("category");
280            entry.setCategory(category);
281
282            doPutExtendedInfos(entry, ctx, null, principal);
283
284        }
285
286        return entry;
287    }
288
289    public List<LogEntry> queryLogsByPage(String[] eventIds, String dateRange, String category, String path,
290            int pageNb, int pageSize) {
291        String[] categories = { category };
292        return queryLogsByPage(eventIds, dateRange, categories, path, pageNb, pageSize);
293    }
294
295    public List<LogEntry> queryLogsByPage(String[] eventIds, Date limit, String category, String path, int pageNb,
296            int pageSize) {
297        String[] categories = { category };
298        return queryLogsByPage(eventIds, limit, categories, path, pageNb, pageSize);
299    }
300
301    @Override
302    public LogEntry newLogEntry() {
303        return new LogEntryImpl();
304    }
305
306    @Override
307    public abstract ExtendedInfo newExtendedInfo(Serializable value);
308
309    protected long syncLogCreationEntries(BaseLogEntryProvider provider, String repoId, String path, Boolean recurs) {
310
311        provider.removeEntries(DocumentEventTypes.DOCUMENT_CREATED, path);
312        try (CoreSession session = CoreInstance.openCoreSession(repoId)) {
313            DocumentRef rootRef = new PathRef(path);
314            DocumentModel root = guardedDocument(session, rootRef);
315            long nbAddedEntries = doSyncNode(provider, session, root, recurs);
316
317            if (log.isDebugEnabled()) {
318                log.debug("synced " + nbAddedEntries + " entries on " + path);
319            }
320
321            return nbAddedEntries;
322        }
323    }
324
325    protected long doSyncNode(BaseLogEntryProvider provider, CoreSession session, DocumentModel node, boolean recurs) {
326
327        long nbSyncedEntries = 1;
328
329        Principal principal = session.getPrincipal();
330        List<DocumentModel> folderishChildren = new ArrayList<DocumentModel>();
331
332        provider.addLogEntry(doCreateAndFillEntryFromDocument(node, session.getPrincipal()));
333
334        for (DocumentModel child : guardedDocumentChildren(session, node.getRef())) {
335            if (child.isFolder() && recurs) {
336                folderishChildren.add(child);
337            } else {
338                provider.addLogEntry(doCreateAndFillEntryFromDocument(child, principal));
339                nbSyncedEntries += 1;
340            }
341        }
342
343        if (recurs) {
344            for (DocumentModel folderChild : folderishChildren) {
345                nbSyncedEntries += doSyncNode(provider, session, folderChild, recurs);
346            }
347        }
348
349        return nbSyncedEntries;
350    }
351
352    // Default implementations to avoid to have too much code to write in actual
353    // implementation
354    //
355    // these methods are actually overridden in the JPA implementation for
356    // optimization purpose
357
358    @Override
359    public void logEvents(EventBundle eventBundle) {
360        boolean processEvents = false;
361        for (String name : getAuditableEventNames()) {
362            if (eventBundle.containsEventName(name)) {
363                processEvents = true;
364                break;
365            }
366        }
367        if (!processEvents) {
368            return;
369        }
370        for (Event event : eventBundle) {
371            logEvent(event);
372        }
373    }
374
375    @Override
376    public void logEvent(Event event) {
377        LogEntry entry = buildEntryFromEvent(event);
378        if (entry != null) {
379            List<LogEntry> entries = new ArrayList<>();
380            entries.add(entry);
381            addLogEntries(entries);
382        }
383    }
384
385    @Override
386    public List<LogEntry> getLogEntriesFor(String uuid) {
387        return getLogEntriesFor(uuid, Collections.<String, FilterMapEntry> emptyMap(), false);
388    }
389
390    @Override
391    public List<?> nativeQuery(String query, int pageNb, int pageSize) {
392        return nativeQuery(query, Collections.<String, Object> emptyMap(), pageNb, pageSize);
393    }
394
395    @Override
396    public List<LogEntry> queryLogs(final String[] eventIds, final String dateRange) {
397        return queryLogsByPage(eventIds, (String) null, (String[]) null, null, 0, 10000);
398    }
399
400    public List<LogEntry> nativeQueryLogs(final String whereClause, final int pageNb, final int pageSize) {
401        List<LogEntry> entries = new LinkedList<>();
402        for (Object entry : nativeQuery(whereClause, pageNb, pageSize)) {
403            if (entry instanceof LogEntry) {
404                entries.add((LogEntry) entry);
405            }
406        }
407        return entries;
408    }
409
410}