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}