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