001/* 002 * (C) Copyright 2006-2017 Nuxeo (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; 022import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_CATEGORY; 023import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_DOC_PATH; 024import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_DOC_UUID; 025import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_EVENT_DATE; 026import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_EVENT_ID; 027import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_ID; 028import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_REPOSITORY_ID; 029import static org.nuxeo.ecm.platform.audit.impl.StreamAuditWriter.COMPUTATION_NAME; 030import static org.nuxeo.ecm.platform.audit.listener.StreamAuditEventListener.DEFAULT_LOG_CONFIG; 031import static org.nuxeo.ecm.platform.audit.listener.StreamAuditEventListener.STREAM_AUDIT_ENABLED_PROP; 032import static org.nuxeo.ecm.platform.audit.listener.StreamAuditEventListener.STREAM_NAME; 033 034import java.io.Serializable; 035import java.security.Principal; 036import java.util.ArrayList; 037import java.util.Calendar; 038import java.util.Collection; 039import java.util.Date; 040import java.util.List; 041import java.util.Map; 042import java.util.Map.Entry; 043import java.util.Set; 044import java.util.concurrent.TimeUnit; 045 046import javax.el.ELException; 047 048import org.apache.commons.lang3.ArrayUtils; 049import org.apache.commons.logging.Log; 050import org.apache.commons.logging.LogFactory; 051import org.jboss.el.ExpressionFactoryImpl; 052import org.nuxeo.ecm.core.api.CoreInstance; 053import org.nuxeo.ecm.core.api.CoreSession; 054import org.nuxeo.ecm.core.api.DocumentModel; 055import org.nuxeo.ecm.core.api.DocumentModelList; 056import org.nuxeo.ecm.core.api.DocumentNotFoundException; 057import org.nuxeo.ecm.core.api.DocumentRef; 058import org.nuxeo.ecm.core.api.LifeCycleConstants; 059import org.nuxeo.ecm.core.api.NuxeoException; 060import org.nuxeo.ecm.core.api.NuxeoPrincipal; 061import org.nuxeo.ecm.core.api.PathRef; 062import org.nuxeo.ecm.core.api.PropertyException; 063import org.nuxeo.ecm.core.api.ScrollResult; 064import org.nuxeo.ecm.core.api.event.DocumentEventTypes; 065import org.nuxeo.ecm.core.api.security.SecurityConstants; 066import org.nuxeo.ecm.core.event.DeletedDocumentModel; 067import org.nuxeo.ecm.core.event.Event; 068import org.nuxeo.ecm.core.event.EventBundle; 069import org.nuxeo.ecm.core.event.EventContext; 070import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 071import org.nuxeo.ecm.core.query.sql.model.Operator; 072import org.nuxeo.ecm.core.query.sql.model.Predicate; 073import org.nuxeo.ecm.platform.audit.api.AuditQueryBuilder; 074import org.nuxeo.ecm.platform.audit.api.AuditStorage; 075import org.nuxeo.ecm.platform.audit.api.ExtendedInfo; 076import org.nuxeo.ecm.platform.audit.api.FilterMapEntry; 077import org.nuxeo.ecm.platform.audit.api.LogEntry; 078import org.nuxeo.ecm.platform.audit.api.OrderByExprs; 079import org.nuxeo.ecm.platform.audit.api.Predicates; 080import org.nuxeo.ecm.platform.audit.impl.LogEntryImpl; 081import org.nuxeo.ecm.platform.audit.service.extension.AdapterDescriptor; 082import org.nuxeo.ecm.platform.audit.service.extension.AuditBackendDescriptor; 083import org.nuxeo.ecm.platform.audit.service.extension.ExtendedInfoDescriptor; 084import org.nuxeo.ecm.platform.el.ExpressionContext; 085import org.nuxeo.ecm.platform.el.ExpressionEvaluator; 086import org.nuxeo.lib.stream.log.LogManager; 087import org.nuxeo.runtime.api.Framework; 088import org.nuxeo.runtime.stream.StreamService; 089 090/** 091 * Abstract class to share code between {@link AuditBackend} implementations 092 * 093 * @author tiry 094 */ 095public abstract class AbstractAuditBackend implements AuditBackend, AuditStorage { 096 097 protected static final Log log = LogFactory.getLog(AbstractAuditBackend.class); 098 099 public static final String FORCE_AUDIT_FACET = "ForceAudit"; 100 101 protected final NXAuditEventsService component; 102 103 protected final AuditBackendDescriptor config; 104 105 protected AbstractAuditBackend(NXAuditEventsService component, AuditBackendDescriptor config) { 106 this.component = component; 107 this.config = config; 108 } 109 110 protected AbstractAuditBackend() { 111 this(null, null); 112 } 113 114 protected final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(new ExpressionFactoryImpl()); 115 116 protected DocumentModel guardedDocument(CoreSession session, DocumentRef reference) { 117 if (session == null) { 118 return null; 119 } 120 if (reference == null) { 121 return null; 122 } 123 try { 124 return session.getDocument(reference); 125 } catch (DocumentNotFoundException e) { 126 return null; 127 } 128 } 129 130 protected DocumentModelList guardedDocumentChildren(CoreSession session, DocumentRef reference) { 131 return session.getChildren(reference); 132 } 133 134 protected LogEntry doCreateAndFillEntryFromDocument(DocumentModel doc, Principal principal) { 135 LogEntry entry = newLogEntry(); 136 entry.setDocPath(doc.getPathAsString()); 137 entry.setDocType(doc.getType()); 138 entry.setDocUUID(doc.getId()); 139 entry.setRepositoryId(doc.getRepositoryName()); 140 entry.setPrincipalName(SecurityConstants.SYSTEM_USERNAME); 141 entry.setCategory("eventDocumentCategory"); 142 entry.setEventId(DocumentEventTypes.DOCUMENT_CREATED); 143 // why hard-code it if we have the document life cycle? 144 entry.setDocLifeCycle("project"); 145 Calendar creationDate = (Calendar) doc.getProperty("dublincore", "created"); 146 if (creationDate != null) { 147 entry.setEventDate(creationDate.getTime()); 148 } 149 150 doPutExtendedInfos(entry, null, doc, principal); 151 152 return entry; 153 } 154 155 protected void doPutExtendedInfos(LogEntry entry, EventContext eventContext, DocumentModel source, 156 Principal principal) { 157 158 ExpressionContext context = new ExpressionContext(); 159 if (eventContext != null) { 160 expressionEvaluator.bindValue(context, "message", eventContext); 161 } 162 if (source != null) { 163 expressionEvaluator.bindValue(context, "source", source); 164 // inject now the adapters 165 for (AdapterDescriptor ad : component.getDocumentAdapters()) { 166 if (source instanceof DeletedDocumentModel) { 167 continue; // skip 168 } 169 Object adapter = source.getAdapter(ad.getKlass()); 170 if (adapter != null) { 171 expressionEvaluator.bindValue(context, ad.getName(), adapter); 172 } 173 } 174 } 175 if (principal != null) { 176 expressionEvaluator.bindValue(context, "principal", principal); 177 } 178 179 // Global extended info 180 populateExtendedInfo(entry, source, context, component.getExtendedInfoDescriptors()); 181 // Event id related extended info 182 populateExtendedInfo(entry, source, context, 183 component.getEventExtendedInfoDescriptors().get(entry.getEventId())); 184 185 if (eventContext != null) { 186 @SuppressWarnings("unchecked") 187 Map<String, Serializable> map = (Map<String, Serializable>) eventContext.getProperty("extendedInfos"); 188 if (map != null) { 189 Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos(); 190 for (Entry<String, Serializable> en : map.entrySet()) { 191 Serializable value = en.getValue(); 192 if (value != null) { 193 extendedInfos.put(en.getKey(), newExtendedInfo(value)); 194 } 195 } 196 } 197 } 198 } 199 200 /** 201 * @since 7.4 202 */ 203 protected void populateExtendedInfo(LogEntry entry, DocumentModel source, ExpressionContext context, 204 Collection<ExtendedInfoDescriptor> extInfos) { 205 if (extInfos != null) { 206 Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos(); 207 for (ExtendedInfoDescriptor descriptor : extInfos) { 208 String exp = descriptor.getExpression(); 209 Serializable value; 210 try { 211 value = expressionEvaluator.evaluateExpression(context, exp, Serializable.class); 212 } catch (PropertyException | UnsupportedOperationException e) { 213 if (source instanceof DeletedDocumentModel) { 214 log.debug("Can not evaluate the expression: " + exp + " on a DeletedDocumentModel, skipping."); 215 } 216 continue; 217 } catch (DocumentNotFoundException e) { 218 if (!DocumentEventTypes.DOCUMENT_REMOVED.equals(entry.getEventId())) { 219 log.error(String.format("Not found: %s, entry: %s", e.getMessage(), entry), e); 220 } 221 continue; 222 } catch (ELException e) { 223 continue; 224 } 225 if (value == null) { 226 continue; 227 } 228 extendedInfos.put(descriptor.getKey(), newExtendedInfo(value)); 229 } 230 } 231 } 232 233 @Override 234 public Set<String> getAuditableEventNames() { 235 return component.getAuditableEventNames(); 236 } 237 238 @Override 239 public LogEntry buildEntryFromEvent(Event event) { 240 EventContext ctx = event.getContext(); 241 String eventName = event.getName(); 242 Date eventDate = new Date(event.getTime()); 243 244 LogEntry entry = newLogEntry(); 245 entry.setEventId(eventName); 246 entry.setEventDate(eventDate); 247 248 if (ctx instanceof DocumentEventContext) { 249 DocumentEventContext docCtx = (DocumentEventContext) ctx; 250 DocumentModel document = docCtx.getSourceDocument(); 251 if (document.hasFacet(SYSTEM_DOCUMENT) && !document.hasFacet(FORCE_AUDIT_FACET)) { 252 // do not log event on System documents 253 // unless it has the FORCE_AUDIT_FACET facet 254 return null; 255 } 256 257 Boolean disabled = (Boolean) docCtx.getProperty(NXAuditEventsService.DISABLE_AUDIT_LOGGER); 258 if (disabled != null && disabled.booleanValue()) { 259 // don't log events with this flag 260 return null; 261 } 262 Principal principal = docCtx.getPrincipal(); 263 Map<String, Serializable> properties = docCtx.getProperties(); 264 265 entry.setDocUUID(document.getId()); 266 entry.setDocPath(document.getPathAsString()); 267 entry.setDocType(document.getType()); 268 entry.setRepositoryId(document.getRepositoryName()); 269 if (principal != null) { 270 String principalName; 271 if (principal instanceof NuxeoPrincipal) { 272 principalName = ((NuxeoPrincipal) principal).getActingUser(); 273 } else { 274 principalName = principal.getName(); 275 } 276 entry.setPrincipalName(principalName); 277 } else { 278 log.warn("received event " + eventName + " with null principal"); 279 } 280 entry.setComment((String) properties.get("comment")); 281 if (document instanceof DeletedDocumentModel) { 282 entry.setComment("Document does not exist anymore!"); 283 } else { 284 if (document.isLifeCycleLoaded()) { 285 entry.setDocLifeCycle(document.getCurrentLifeCycleState()); 286 } 287 } 288 if (LifeCycleConstants.TRANSITION_EVENT.equals(eventName)) { 289 entry.setDocLifeCycle((String) docCtx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TO)); 290 } 291 String category = (String) properties.get("category"); 292 if (category != null) { 293 entry.setCategory(category); 294 } else { 295 entry.setCategory("eventDocumentCategory"); 296 } 297 298 doPutExtendedInfos(entry, docCtx, document, principal); 299 300 } else { 301 Principal principal = ctx.getPrincipal(); 302 Map<String, Serializable> properties = ctx.getProperties(); 303 304 if (principal != null) { 305 String principalName; 306 if (principal instanceof NuxeoPrincipal) { 307 principalName = ((NuxeoPrincipal) principal).getActingUser(); 308 } else { 309 principalName = principal.getName(); 310 } 311 entry.setPrincipalName(principalName); 312 } 313 entry.setComment((String) properties.get("comment")); 314 315 String category = (String) properties.get("category"); 316 entry.setCategory(category); 317 318 doPutExtendedInfos(entry, ctx, null, principal); 319 320 } 321 322 return entry; 323 } 324 325 @Override 326 public LogEntry newLogEntry() { 327 return new LogEntryImpl(); 328 } 329 330 @Override 331 public abstract ExtendedInfo newExtendedInfo(Serializable value); 332 333 protected long syncLogCreationEntries(BaseLogEntryProvider provider, String repoId, String path, Boolean recurs) { 334 335 provider.removeEntries(DocumentEventTypes.DOCUMENT_CREATED, path); 336 try (CoreSession session = CoreInstance.openCoreSession(repoId)) { 337 DocumentRef rootRef = new PathRef(path); 338 DocumentModel root = guardedDocument(session, rootRef); 339 long nbAddedEntries = doSyncNode(provider, session, root, recurs); 340 341 if (log.isDebugEnabled()) { 342 log.debug("synced " + nbAddedEntries + " entries on " + path); 343 } 344 345 return nbAddedEntries; 346 } 347 } 348 349 protected long doSyncNode(BaseLogEntryProvider provider, CoreSession session, DocumentModel node, boolean recurs) { 350 351 long nbSyncedEntries = 1; 352 353 Principal principal = session.getPrincipal(); 354 List<DocumentModel> folderishChildren = new ArrayList<>(); 355 356 provider.addLogEntry(doCreateAndFillEntryFromDocument(node, session.getPrincipal())); 357 358 for (DocumentModel child : guardedDocumentChildren(session, node.getRef())) { 359 if (child.isFolder() && recurs) { 360 folderishChildren.add(child); 361 } else { 362 provider.addLogEntry(doCreateAndFillEntryFromDocument(child, principal)); 363 nbSyncedEntries += 1; 364 } 365 } 366 367 if (recurs) { 368 for (DocumentModel folderChild : folderishChildren) { 369 nbSyncedEntries += doSyncNode(provider, session, folderChild, recurs); 370 } 371 } 372 373 return nbSyncedEntries; 374 } 375 376 @Override 377 public void logEvents(EventBundle bundle) { 378 if (!isAuditable(bundle)) { 379 return; 380 } 381 for (Event event : bundle) { 382 logEvent(event); 383 } 384 } 385 386 protected boolean isAuditable(EventBundle eventBundle) { 387 for (String name : getAuditableEventNames()) { 388 if (eventBundle.containsEventName(name)) { 389 return true; 390 } 391 } 392 return false; 393 } 394 395 @Override 396 public void logEvent(Event event) { 397 if (!getAuditableEventNames().contains(event.getName())) { 398 return; 399 } 400 LogEntry entry = buildEntryFromEvent(event); 401 if (entry == null) { 402 return; 403 } 404 component.bulker.offer(entry); 405 } 406 407 @Override 408 public boolean await(long time, TimeUnit unit) throws InterruptedException { 409 if (Framework.isBooleanPropertyTrue(STREAM_AUDIT_ENABLED_PROP)) { 410 StreamService service = Framework.getService(StreamService.class); 411 LogManager logManager = service.getLogManager(DEFAULT_LOG_CONFIG); 412 // when there is no lag between producer and consumer we are done 413 long deadline = System.currentTimeMillis() + unit.toMillis(time); 414 while (logManager.getLag(STREAM_NAME, COMPUTATION_NAME).lag() > 0) { 415 if (System.currentTimeMillis() > deadline) { 416 return false; 417 } 418 Thread.sleep(50); 419 } 420 return true; 421 } else { 422 return component.bulker.await(time, unit); 423 } 424 } 425 426 @Override 427 @Deprecated 428 public List<LogEntry> getLogEntriesFor(String uuid, Map<String, FilterMapEntry> filterMap, boolean doDefaultSort) { 429 // create builder 430 AuditQueryBuilder builder = new AuditQueryBuilder(); 431 // create predicates 432 builder.addAndPredicate(Predicates.eq(LOG_DOC_UUID, uuid)); 433 filterMap.values().stream().map(this::convert).forEach(builder::addAndPredicate); 434 if (doDefaultSort) { 435 builder.defaultOrder(); 436 } 437 return queryLogs(builder); 438 } 439 440 protected Predicate convert(FilterMapEntry entry) { 441 String name = entry.getColumnName(); 442 String operator = entry.getOperator(); 443 Object value = entry.getObject(); 444 if (Operator.EQ.toString().equals(operator)) { 445 return Predicates.eq(name, value); 446 } else if (Operator.LT.toString().equals(operator)) { 447 return Predicates.lt(name, value); 448 } else if (Operator.LTEQ.toString().equals(operator)) { 449 return Predicates.lte(name, value); 450 } else if (Operator.GTEQ.toString().equals(operator)) { 451 return Predicates.gte(name, value); 452 } else if (Operator.GT.toString().equals(operator)) { 453 return Predicates.gt(name, value); 454 } else if (Operator.IN.toString().equals(operator)) { 455 return Predicates.in(name, (List<?>) value); 456 } 457 throw new NuxeoException(String.format("Audit backend search doesn't handle '%s' operator", operator)); 458 } 459 460 @Override 461 public List<LogEntry> queryLogsByPage(String[] eventIds, Date limit, String[] categories, String path, int pageNb, 462 int pageSize) { 463 AuditQueryBuilder builder = new AuditQueryBuilder(); 464 if (ArrayUtils.isNotEmpty(eventIds)) { 465 if (eventIds.length == 1) { 466 builder.addAndPredicate(Predicates.eq(LOG_EVENT_ID, eventIds[0])); 467 } else { 468 builder.addAndPredicate(Predicates.in(LOG_EVENT_ID, eventIds[0])); 469 } 470 } 471 if (ArrayUtils.isNotEmpty(categories)) { 472 if (categories.length == 1) { 473 builder.addAndPredicate(Predicates.eq(LOG_CATEGORY, categories[0])); 474 } else { 475 builder.addAndPredicate(Predicates.in(LOG_CATEGORY, categories[0])); 476 } 477 } 478 if (path != null) { 479 builder.addAndPredicate(Predicates.eq(LOG_DOC_PATH, path)); 480 } 481 if (limit != null) { 482 builder.addAndPredicate(Predicates.lt(LOG_EVENT_DATE, limit)); 483 } 484 builder.offset(pageNb * pageSize).limit(pageSize); 485 return queryLogs(builder); 486 } 487 488 @Override 489 public long getLatestLogId(String repositoryId, String... eventIds) { 490 AuditQueryBuilder builder = new AuditQueryBuilder().addAndPredicate( 491 Predicates.eq(LOG_REPOSITORY_ID, repositoryId)) 492 .addAndPredicate(Predicates.in(LOG_EVENT_ID, eventIds)) 493 .order(OrderByExprs.desc(LOG_ID)) 494 .limit(1); 495 return queryLogs(builder).stream().mapToLong(LogEntry::getId).findFirst().orElse(0L); 496 } 497 498 @Override 499 public List<LogEntry> getLogEntriesAfter(long logIdOffset, int limit, String repositoryId, String... eventIds) { 500 AuditQueryBuilder builder = new AuditQueryBuilder().addAndPredicate( 501 Predicates.eq(LOG_REPOSITORY_ID, repositoryId)) 502 .addAndPredicate(Predicates.in(LOG_EVENT_ID, eventIds)) 503 .addAndPredicate(Predicates.gte(LOG_ID, logIdOffset)) 504 .order(OrderByExprs.asc(LOG_ID)) 505 .limit(limit); 506 return queryLogs(builder); 507 } 508 509 @Override 510 public void restore(AuditStorage auditStorage, int batchSize, int keepAlive) { 511 512 AuditQueryBuilder builder = new AuditQueryBuilder(); 513 ScrollResult<String> scrollResult = auditStorage.scroll(builder, batchSize, keepAlive); 514 long t0 = System.currentTimeMillis(); 515 int total = 0; 516 517 log.info("Starting audit restoration"); 518 519 while (scrollResult.hasResults()) { 520 List<String> jsonEntries = scrollResult.getResults(); 521 total += jsonEntries.size(); 522 append(jsonEntries); 523 524 double dt = (System.currentTimeMillis() - t0) / 1000.0; 525 if (dt != 0) { 526 if (log.isDebugEnabled()) { 527 log.debug("Restoration speed: " + (total / dt) + " entries/s"); 528 } 529 } 530 531 scrollResult = auditStorage.scroll(scrollResult.getScrollId()); 532 } 533 534 log.info("Audit restoration done: " + total + " entries migrated from the audit storage"); 535 536 } 537 538}