001/* 002 * (C) Copyright 2017 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 * Kevin Leturc 018 */ 019package org.nuxeo.mongodb.audit; 020 021import static org.nuxeo.mongodb.audit.LogEntryConstants.PROPERTY_CATEGORY; 022import static org.nuxeo.mongodb.audit.LogEntryConstants.PROPERTY_DOC_PATH; 023import static org.nuxeo.mongodb.audit.LogEntryConstants.PROPERTY_DOC_UUID; 024import static org.nuxeo.mongodb.audit.LogEntryConstants.PROPERTY_EVENT_DATE; 025import static org.nuxeo.mongodb.audit.LogEntryConstants.PROPERTY_EVENT_ID; 026import static org.nuxeo.mongodb.audit.LogEntryConstants.PROPERTY_REPOSITORY_ID; 027 028import java.io.Serializable; 029import java.text.SimpleDateFormat; 030import java.util.ArrayList; 031import java.util.Calendar; 032import java.util.Collections; 033import java.util.Date; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.Map.Entry; 038import java.util.TimeZone; 039import java.util.stream.Collectors; 040import java.util.stream.StreamSupport; 041 042import org.apache.commons.collections.MapUtils; 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.bson.Document; 046import org.bson.conversions.Bson; 047import org.nuxeo.common.utils.TextTemplate; 048import org.nuxeo.ecm.core.uidgen.UIDGeneratorService; 049import org.nuxeo.ecm.core.uidgen.UIDSequencer; 050import org.nuxeo.ecm.platform.audit.api.ExtendedInfo; 051import org.nuxeo.ecm.platform.audit.api.FilterMapEntry; 052import org.nuxeo.ecm.platform.audit.api.LogEntry; 053import org.nuxeo.ecm.platform.audit.api.query.AuditQueryException; 054import org.nuxeo.ecm.platform.audit.api.query.DateRangeParser; 055import org.nuxeo.ecm.platform.audit.service.AbstractAuditBackend; 056import org.nuxeo.ecm.platform.audit.service.AuditBackend; 057import org.nuxeo.ecm.platform.audit.service.BaseLogEntryProvider; 058import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService; 059import org.nuxeo.ecm.platform.audit.service.extension.AuditBackendDescriptor; 060import org.nuxeo.mongodb.core.MongoDBComponent; 061import org.nuxeo.mongodb.core.MongoDBConnectionService; 062import org.nuxeo.mongodb.core.MongoDBSerializationHelper; 063import org.nuxeo.runtime.api.Framework; 064import org.nuxeo.runtime.model.DefaultComponent; 065import org.nuxeo.runtime.services.config.ConfigurationService; 066 067import com.mongodb.client.FindIterable; 068import com.mongodb.client.MongoCollection; 069import com.mongodb.client.MongoDatabase; 070import com.mongodb.client.model.Filters; 071import com.mongodb.client.model.Sorts; 072 073/** 074 * Implementation of the {@link AuditBackend} interface using MongoDB persistence. 075 * 076 * @since 9.1 077 */ 078public class MongoDBAuditBackend extends AbstractAuditBackend implements AuditBackend { 079 080 private static final Log log = LogFactory.getLog(MongoDBAuditBackend.class); 081 082 public static final String AUDIT_DATABASE_ID = "audit"; 083 084 public static final String COLLECTION_NAME_PROPERTY = "nuxeo.mongodb.audit.collection.name"; 085 086 public static final String DEFAULT_COLLECTION_NAME = "audit"; 087 088 public static final String SEQ_NAME = "audit"; 089 090 protected MongoCollection<Document> collection; 091 092 protected MongoDBLogEntryProvider provider = new MongoDBLogEntryProvider(); 093 094 public MongoDBAuditBackend(NXAuditEventsService component, AuditBackendDescriptor config) { 095 super(component, config); 096 } 097 098 @Override 099 public int getApplicationStartedOrder() { 100 DefaultComponent component = (DefaultComponent) Framework.getRuntime().getComponent(MongoDBComponent.NAME); 101 return component.getApplicationStartedOrder() + 1; 102 } 103 104 @Override 105 public void onApplicationStarted() { 106 log.info("Activate MongoDB backend for Audit"); 107 // First retrieve the collection name 108 ConfigurationService configurationService = Framework.getService(ConfigurationService.class); 109 String collName = configurationService.getProperty(COLLECTION_NAME_PROPERTY, DEFAULT_COLLECTION_NAME); 110 // Get a connection to MongoDB 111 MongoDBConnectionService mongoService = Framework.getService(MongoDBConnectionService.class); 112 MongoDatabase database = mongoService.getDatabase(AUDIT_DATABASE_ID); 113 collection = database.getCollection(collName); 114 // TODO migration ? 115 } 116 117 @Override 118 public void onShutdown() { 119 if (collection != null) { 120 collection = null; 121 } 122 } 123 124 /** 125 * @return the {@link MongoCollection} configured with audit settings. 126 */ 127 public MongoCollection<Document> getAuditCollection() { 128 return collection; 129 } 130 131 @Override 132 public List<LogEntry> getLogEntriesFor(String uuid, String repositoryId) { 133 Bson docFilter = Filters.eq(PROPERTY_DOC_UUID, uuid); 134 Bson repoFilter = Filters.eq(PROPERTY_REPOSITORY_ID, repositoryId); 135 Bson query = Filters.and(docFilter, repoFilter); 136 return getLogEntries(query, false); 137 } 138 139 @Override 140 public List<LogEntry> getLogEntriesFor(String uuid, Map<String, FilterMapEntry> filterMap, boolean doDefaultSort) { 141 Bson docFilter = Filters.eq(PROPERTY_DOC_UUID, uuid); 142 143 Bson filter; 144 if (MapUtils.isEmpty(filterMap)) { 145 filter = docFilter; 146 } else { 147 List<Bson> list = new ArrayList<>(Collections.singleton(docFilter)); 148 for (Entry<String, FilterMapEntry> entry : filterMap.entrySet()) { 149 FilterMapEntry filterEntry = entry.getValue(); 150 list.add(Filters.eq(filterEntry.getColumnName(), filterEntry.getObject())); 151 } 152 filter = Filters.and(list); 153 } 154 return getLogEntries(filter, doDefaultSort); 155 } 156 157 @Override 158 public LogEntry getLogEntryByID(long id) { 159 Document document = collection.find(Filters.eq(MongoDBSerializationHelper.MONGODB_ID, Long.valueOf(id))) 160 .first(); 161 if (document == null) { 162 return null; 163 } 164 return MongoDBAuditEntryReader.read(document); 165 } 166 167 @Override 168 public List<?> nativeQuery(String query, Map<String, Object> params, int pageNb, int pageSize) { 169 Bson filter = buildFilter(query, params); 170 logRequest(filter, pageNb, pageSize); 171 FindIterable<Document> iterable = collection.find(filter).skip(pageNb * pageSize).limit(pageSize); 172 return buildLogEntries(iterable); 173 } 174 175 public Bson buildFilter(String query, Map<String, Object> params) { 176 if (params != null && params.size() > 0) { 177 query = expandQueryVariables(query, params); 178 } 179 return Document.parse(query); 180 } 181 182 public String expandQueryVariables(String query, Object[] params) { 183 Map<String, Object> qParams = new HashMap<>(); 184 for (int i = 0; i < params.length; i++) { 185 query = query.replaceFirst("\\?", "\\${param" + i + "}"); 186 qParams.put("param" + i, params[i]); 187 } 188 return expandQueryVariables(query, qParams); 189 } 190 191 public String expandQueryVariables(String query, Map<String, Object> params) { 192 if (params != null && params.size() > 0) { 193 TextTemplate tmpl = new TextTemplate(); 194 // MongoDB date formatter - copied from org.bson.json.JsonWriter 195 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'"); 196 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 197 for (Entry<String, Object> entry : params.entrySet()) { 198 String key = entry.getKey(); 199 Object value = entry.getValue(); 200 if (value instanceof Calendar) { 201 tmpl.setVariable(key, dateFormat.format(((Calendar) value).getTime())); 202 } else if (value instanceof Date) { 203 tmpl.setVariable(key, dateFormat.format(value)); 204 } else if (value != null) { 205 tmpl.setVariable(key, value.toString()); 206 } 207 } 208 query = tmpl.processText(query); 209 } 210 return query; 211 } 212 213 @Override 214 public List<LogEntry> queryLogsByPage(String[] eventIds, Date limit, String[] categories, String path, int pageNb, 215 int pageSize) { 216 List<Bson> list = new ArrayList<>(); 217 if (eventIds != null && eventIds.length > 0) { 218 if (eventIds.length == 1) { 219 list.add(Filters.eq(PROPERTY_EVENT_ID, eventIds[0])); 220 } else { 221 list.add(Filters.in(PROPERTY_EVENT_ID, eventIds)); 222 } 223 } 224 if (categories != null && categories.length > 0) { 225 if (categories.length == 1) { 226 list.add(Filters.eq(PROPERTY_CATEGORY, categories[0])); 227 } else { 228 list.add(Filters.in(PROPERTY_CATEGORY, categories)); 229 } 230 } 231 if (path != null) { 232 list.add(Filters.eq(PROPERTY_DOC_PATH, path)); 233 } 234 if (limit != null) { 235 list.add(Filters.lt(PROPERTY_EVENT_DATE, limit)); 236 } 237 Bson filter = list.size() == 1 ? list.get(0) : Filters.and(list); 238 logRequest(filter, pageNb, pageSize); 239 FindIterable<Document> iterable = collection.find(filter).skip(pageNb * pageSize).limit(pageSize); 240 return buildLogEntries(iterable); 241 } 242 243 @Override 244 public List<LogEntry> queryLogsByPage(String[] eventIds, String dateRange, String[] categories, String path, 245 int pageNb, int pageSize) { 246 // TODO maybe we can put this method in AbstratAuditBackend ? 247 Date limit = null; 248 if (dateRange != null) { 249 try { 250 limit = DateRangeParser.parseDateRangeQuery(new Date(), dateRange); 251 } catch (AuditQueryException aqe) { 252 aqe.addInfo("Wrong date range query. Query was " + dateRange); 253 throw aqe; 254 } 255 } 256 return queryLogsByPage(eventIds, limit, categories, path, pageNb, pageSize); 257 } 258 259 @Override 260 public void addLogEntries(List<LogEntry> entries) { 261 if (entries.isEmpty()) { 262 return; 263 } 264 265 UIDGeneratorService uidGeneratorService = Framework.getService(UIDGeneratorService.class); 266 UIDSequencer seq = uidGeneratorService.getSequencer(); 267 268 List<Document> documents = new ArrayList<>(entries.size()); 269 for (LogEntry entry : entries) { 270 entry.setId(seq.getNextLong(SEQ_NAME)); 271 if (log.isDebugEnabled()) { 272 log.debug(String.format("Indexing log enry Id: %s, with logDate : %s, for docUUID: %s ", 273 Long.valueOf(entry.getId()), entry.getLogDate(), entry.getDocUUID())); 274 } 275 documents.add(MongoDBAuditEntryWriter.asDocument(entry)); 276 } 277 collection.insertMany(documents); 278 } 279 280 @Override 281 public Long getEventsCount(String eventId) { 282 return Long.valueOf(collection.count(Filters.eq("eventId", eventId))); 283 } 284 285 @Override 286 public long syncLogCreationEntries(String repoId, String path, Boolean recurs) { 287 return syncLogCreationEntries(provider, repoId, path, recurs); 288 } 289 290 @Override 291 public ExtendedInfo newExtendedInfo(Serializable value) { 292 return new MongoDBExtendedInfo(value); 293 } 294 295 private List<LogEntry> getLogEntries(Bson filter, boolean doDefaultSort) { 296 Bson orderBy = null; 297 if (doDefaultSort) { 298 orderBy = Sorts.descending(PROPERTY_EVENT_DATE); 299 } 300 301 logRequest(filter, orderBy); 302 FindIterable<Document> iterable = collection.find(filter).sort(orderBy); 303 return buildLogEntries(iterable); 304 } 305 306 private List<LogEntry> buildLogEntries(FindIterable<Document> iterable) { 307 return StreamSupport.stream(iterable.spliterator(), false) 308 .map(MongoDBAuditEntryReader::read) 309 .collect(Collectors.toList()); 310 } 311 312 private void logRequest(Bson filter, Bson orderBy) { 313 if (log.isDebugEnabled()) { 314 log.debug("MongoDB: FILTER " + filter + (orderBy == null ? "" : " ORDER BY " + orderBy)); 315 } 316 } 317 318 private void logRequest(Bson filter, int pageNb, int pageSize) { 319 if (log.isDebugEnabled()) { 320 log.debug("MongoDB: FILTER " + filter + " OFFSET " + pageNb + " LIMIT " + pageSize); 321 } 322 } 323 324 public class MongoDBLogEntryProvider implements BaseLogEntryProvider { 325 326 @Override 327 public int removeEntries(String eventId, String pathPattern) { 328 throw new UnsupportedOperationException("Not implemented yet!"); 329 } 330 331 @Override 332 public void addLogEntry(LogEntry logEntry) { 333 List<LogEntry> entries = new ArrayList<>(); 334 entries.add(logEntry); 335 addLogEntries(entries); 336 } 337 338 @Override 339 public List<LogEntry> getLogEntriesFor(String uuid, String repositoryId) { 340 throw new UnsupportedOperationException("Not implemented yet!"); 341 } 342 343 @Override 344 public List<LogEntry> getLogEntriesFor(String uuid) { 345 throw new UnsupportedOperationException("Not implemented yet!"); 346 } 347 348 @Override 349 public List<LogEntry> getLogEntriesFor(String uuid, Map<String, FilterMapEntry> filterMap, 350 boolean doDefaultSort) { 351 throw new UnsupportedOperationException("Not implemented yet!"); 352 } 353 354 } 355 356}