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}