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, component.getEventExtendedInfoDescriptors().get(entry.getEventId())); 158 159 if (eventContext != null) { 160 @SuppressWarnings("unchecked") 161 Map<String, Serializable> map = (Map<String, Serializable>) eventContext.getProperty("extendedInfos"); 162 if (map != null) { 163 Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos(); 164 for (Entry<String, Serializable> en : map.entrySet()) { 165 Serializable value = en.getValue(); 166 if (value != null) { 167 extendedInfos.put(en.getKey(), newExtendedInfo(value)); 168 } 169 } 170 } 171 } 172 } 173 174 /** 175 * @since 7.4 176 */ 177 protected void populateExtendedInfo(LogEntry entry, DocumentModel source, ExpressionContext context, 178 Collection<ExtendedInfoDescriptor> extInfos) { 179 if (extInfos != null) { 180 Map<String, ExtendedInfo> extendedInfos = entry.getExtendedInfos(); 181 for (ExtendedInfoDescriptor descriptor : extInfos) { 182 String exp = descriptor.getExpression(); 183 Serializable value = null; 184 try { 185 value = expressionEvaluator.evaluateExpression(context, exp, Serializable.class); 186 } catch (PropertyException | UnsupportedOperationException e) { 187 if (source instanceof DeletedDocumentModel) { 188 log.debug("Can not evaluate the expression: " + exp + " on a DeletedDocumentModel, skipping."); 189 } 190 continue; 191 } catch (ELException e) { 192 continue; 193 } 194 if (value == null) { 195 continue; 196 } 197 extendedInfos.put(descriptor.getKey(), newExtendedInfo(value)); 198 } 199 } 200 } 201 202 @Override 203 public Set<String> getAuditableEventNames() { 204 return component.getAuditableEventNames(); 205 } 206 207 protected LogEntry buildEntryFromEvent(Event event) { 208 EventContext ctx = event.getContext(); 209 String eventName = event.getName(); 210 Date eventDate = new Date(event.getTime()); 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 @Override 298 public List<LogEntry> queryLogsByPage(String[] eventIds, String dateRange, String category, String path, 299 int pageNb, int pageSize) { 300 String[] categories = { category }; 301 return queryLogsByPage(eventIds, dateRange, categories, path, pageNb, pageSize); 302 } 303 304 @Override 305 public List<LogEntry> queryLogsByPage(String[] eventIds, Date limit, String category, String path, int pageNb, 306 int pageSize) { 307 String[] categories = { category }; 308 return queryLogsByPage(eventIds, limit, categories, path, pageNb, pageSize); 309 } 310 311 @Override 312 public LogEntry newLogEntry() { 313 return new LogEntryImpl(); 314 } 315 316 @Override 317 public abstract ExtendedInfo newExtendedInfo(Serializable value); 318 319 protected long syncLogCreationEntries(BaseLogEntryProvider provider, String repoId, String path, Boolean recurs) { 320 321 provider.removeEntries(DocumentEventTypes.DOCUMENT_CREATED, path); 322 try (CoreSession session = CoreInstance.openCoreSession(repoId)) { 323 DocumentRef rootRef = new PathRef(path); 324 DocumentModel root = guardedDocument(session, rootRef); 325 long nbAddedEntries = doSyncNode(provider, session, root, recurs); 326 327 if (log.isDebugEnabled()) { 328 log.debug("synced " + nbAddedEntries + " entries on " + path); 329 } 330 331 return nbAddedEntries; 332 } 333 } 334 335 protected long doSyncNode(BaseLogEntryProvider provider, CoreSession session, DocumentModel node, boolean recurs) { 336 337 long nbSyncedEntries = 1; 338 339 Principal principal = session.getPrincipal(); 340 List<DocumentModel> folderishChildren = new ArrayList<DocumentModel>(); 341 342 provider.addLogEntry(doCreateAndFillEntryFromDocument(node, session.getPrincipal())); 343 344 for (DocumentModel child : guardedDocumentChildren(session, node.getRef())) { 345 if (child.isFolder() && recurs) { 346 folderishChildren.add(child); 347 } else { 348 provider.addLogEntry(doCreateAndFillEntryFromDocument(child, principal)); 349 nbSyncedEntries += 1; 350 } 351 } 352 353 if (recurs) { 354 for (DocumentModel folderChild : folderishChildren) { 355 nbSyncedEntries += doSyncNode(provider, session, folderChild, recurs); 356 } 357 } 358 359 return nbSyncedEntries; 360 } 361 362 @Override 363 public void logEvents(EventBundle bundle) { 364 if (!isAuditable(bundle)) { 365 return; 366 } 367 for (Event event : bundle) { 368 logEvent(event); 369 } 370 } 371 372 protected boolean isAuditable(EventBundle eventBundle) { 373 for (String name : getAuditableEventNames()) { 374 if (eventBundle.containsEventName(name)) { 375 return true; 376 } 377 } 378 return false; 379 } 380 381 @Override 382 public void logEvent(Event event) { 383 if (!getAuditableEventNames().contains(event.getName())) { 384 return; 385 } 386 LogEntry entry = buildEntryFromEvent(event); 387 if (entry == null) { 388 return; 389 } 390 component.bulker.offer(entry); 391 } 392 393 @Override 394 public boolean await(long time, TimeUnit unit) throws InterruptedException { 395 return component.bulker.await(time, unit); 396 } 397 398 @Override 399 public List<LogEntry> getLogEntriesFor(String uuid) { 400 return getLogEntriesFor(uuid, Collections.<String, FilterMapEntry> emptyMap(), false); 401 } 402 403 @Override 404 public List<?> nativeQuery(String query, int pageNb, int pageSize) { 405 return nativeQuery(query, Collections.<String, Object> emptyMap(), pageNb, pageSize); 406 } 407 408 @Override 409 public List<LogEntry> queryLogs(final String[] eventIds, final String dateRange) { 410 return queryLogsByPage(eventIds, (String) null, (String[]) null, null, 0, 10000); 411 } 412 413 @Override 414 public List<LogEntry> nativeQueryLogs(final String whereClause, final int pageNb, final int pageSize) { 415 List<LogEntry> entries = new LinkedList<>(); 416 for (Object entry : nativeQuery(whereClause, pageNb, pageSize)) { 417 if (entry instanceof LogEntry) { 418 entries.add((LogEntry) entry); 419 } 420 } 421 return entries; 422 } 423 424}