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