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}