diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/AuditLogType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/AuditLogType.java new file mode 100644 index 00000000..97dab44e --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/AuditLogType.java @@ -0,0 +1,23 @@ +package com.sismics.docs.core.constant; + +/** + * Audit log types. + * + * @author bgamard + */ +public enum AuditLogType { + /** + * Create. + */ + CREATE, + + /** + * Update. + */ + UPDATE, + + /** + * Delete. + */ + DELETE +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java index 97570e47..5609fd98 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java @@ -9,9 +9,11 @@ import javax.persistence.EntityManager; import javax.persistence.Query; import com.sismics.docs.core.constant.AclTargetType; +import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.dto.AclDto; import com.sismics.docs.core.model.jpa.Acl; +import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.util.context.ThreadLocalContext; /** @@ -35,6 +37,9 @@ public class AclDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); em.persist(acl); + // Create audit log + AuditLogUtil.create(acl, AuditLogType.CREATE); + return acl.getId(); } @@ -121,9 +126,22 @@ public class AclDao { * @param perm Permission * @param targetId Target ID */ + @SuppressWarnings("unchecked") public void delete(String sourceId, PermType perm, String targetId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId"); + + // Create audit log + Query q = em.createQuery("from Acl a where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId"); + q.setParameter("sourceId", sourceId); + q.setParameter("perm", perm); + q.setParameter("targetId", targetId); + List aclList = q.getResultList(); + for (Acl acl : aclList) { + AuditLogUtil.create(acl, AuditLogType.DELETE); + } + + // Soft delete the ACLs + q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId"); q.setParameter("sourceId", sourceId); q.setParameter("perm", perm); q.setParameter("targetId", targetId); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java new file mode 100644 index 00000000..bb63923d --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java @@ -0,0 +1,108 @@ +package com.sismics.docs.core.dao.jpa; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.persistence.EntityManager; + +import com.google.common.base.Joiner; +import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.dao.jpa.criteria.AuditLogCriteria; +import com.sismics.docs.core.dao.jpa.dto.AuditLogDto; +import com.sismics.docs.core.model.jpa.AuditLog; +import com.sismics.docs.core.util.jpa.PaginatedList; +import com.sismics.docs.core.util.jpa.PaginatedLists; +import com.sismics.docs.core.util.jpa.QueryParam; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.util.context.ThreadLocalContext; + +/** + * Audit log DAO. + * + * @author bgamard + */ +public class AuditLogDao { + /** + * Creates a new audit log. + * + * @param auditLog Audit log + * @return New ID + * @throws Exception + */ + public String create(AuditLog auditLog) { + // Create the UUID + auditLog.setId(UUID.randomUUID().toString()); + + // Create the audit log + EntityManager em = ThreadLocalContext.get().getEntityManager(); + auditLog.setCreateDate(new Date()); + em.persist(auditLog); + + return auditLog.getId(); + } + + /** + * Searches audit logs by criteria. + * + * @param paginatedList List of audit logs (updated by side effects) + * @param criteria Search criteria + * @param sortCriteria Sort criteria + * @return List of audit logs + * @throws Exception + */ + public void findByCriteria(PaginatedList paginatedList, AuditLogCriteria criteria, SortCriteria sortCriteria) throws Exception { + Map parameterMap = new HashMap(); + List criteriaList = new ArrayList(); + + StringBuilder sb = new StringBuilder("select l.LOG_ID_C c0, l.LOG_CREATEDATE_D c1, l.LOG_IDENTITY_C c2, l.LOG_CLASSENTITY_C c3, l.LOG_TYPE_C c4, l.LOG_MESSAGE_C c5 "); + sb.append(" from T_AUDIT_LOG l "); + + // Adds search criteria + if (criteria.getDocumentId() != null) { + // ACL on document is not checked here, it's assumed + StringBuilder sb0 = new StringBuilder(" (l.LOG_IDENTITY_C = :documentId and l.LOG_CLASSENTITY_C = 'Document' "); + sb0.append(" or l.LOG_IDENTITY_C in (select f.FIL_ID_C from T_FILE f where f.FIL_IDDOC_C = :documentId) and l.LOG_CLASSENTITY_C = 'File' "); + sb0.append(" or l.LOG_IDENTITY_C in (select a.ACL_ID_C from T_ACL a where a.ACL_SOURCEID_C = :documentId) and l.LOG_CLASSENTITY_C = 'Acl') "); + criteriaList.add(sb0.toString()); + parameterMap.put("documentId", criteria.getDocumentId()); + } + + if (criteria.getUserId() != null) { + StringBuilder sb0 = new StringBuilder(" (l.LOG_IDENTITY_C = :userId and l.LOG_CLASSENTITY_C = 'User' "); + sb0.append(" or l.LOG_IDENTITY_C in (select t.TAG_ID_C from T_TAG t where t.TAG_IDUSER_C = :userId) and l.LOG_CLASSENTITY_C = 'Tag' "); + sb0.append(" or l.LOG_IDENTITY_C in (select d.DOC_ID_C from T_DOCUMENT d where d.DOC_IDUSER_C = :userId) and l.LOG_CLASSENTITY_C = 'Document') "); + criteriaList.add(sb0.toString()); + parameterMap.put("userId", criteria.getUserId()); + } + + if (!criteriaList.isEmpty()) { + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); + } + + // Perform the search + QueryParam queryParam = new QueryParam(sb.toString(), parameterMap); + List l = PaginatedLists.executePaginatedQuery(paginatedList, queryParam, sortCriteria); + + // Assemble results + List auditLogDtoList = new ArrayList(); + for (Object[] o : l) { + int i = 0; + AuditLogDto auditLogDto = new AuditLogDto(); + auditLogDto.setId((String) o[i++]); + auditLogDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); + auditLogDto.setEntityId((String) o[i++]); + auditLogDto.setEntityClass((String) o[i++]); + auditLogDto.setType(AuditLogType.valueOf((String) o[i++])); + auditLogDto.setMessage((String) o[i++]); + auditLogDtoList.add(auditLogDto); + } + + paginatedList.setResultList(auditLogDtoList); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java index 28e509b3..f6ae41f1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java @@ -15,11 +15,13 @@ import javax.persistence.Query; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.dao.lucene.LuceneDao; import com.sismics.docs.core.model.jpa.Document; +import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.QueryParam; @@ -47,6 +49,9 @@ public class DocumentDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); em.persist(document); + // Create audit log + AuditLogUtil.create(document, AuditLogType.CREATE); + return document.getId(); } @@ -145,6 +150,9 @@ public class DocumentDao { q.setParameter("documentId", id); q.setParameter("dateNow", dateNow); q.executeUpdate(); + + // Create audit log + AuditLogUtil.create(documentDb, AuditLogType.DELETE); } /** @@ -167,6 +175,7 @@ public class DocumentDao { * * @param paginatedList List of documents (updated by side effects) * @param criteria Search criteria + * @param sortCriteria Sort criteria * @return List of documents * @throws Exception */ @@ -248,4 +257,30 @@ public class DocumentDao { paginatedList.setResultList(documentDtoList); } + + /** + * Update a document. + * + * @param document Document to update + * @return Updated document + */ + public Document update(Document document) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the document + Query q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null"); + q.setParameter("id", document.getId()); + Document documentFromDb = (Document) q.getSingleResult(); + + // Update the document + documentFromDb.setTitle(document.getTitle()); + documentFromDb.setDescription(document.getDescription()); + documentFromDb.setCreateDate(document.getCreateDate()); + documentFromDb.setLanguage(document.getLanguage()); + + // Create audit log + AuditLogUtil.create(documentFromDb, AuditLogType.UPDATE); + + return documentFromDb; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java index 6156a126..54da76f9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java @@ -8,7 +8,9 @@ import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.Query; +import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.util.context.ThreadLocalContext; /** @@ -33,6 +35,9 @@ public class FileDao { file.setCreateDate(new Date()); em.persist(file); + // Create audit log + AuditLogUtil.create(file, AuditLogType.CREATE); + return file.getId(); } @@ -92,6 +97,9 @@ public class FileDao { // Delete the file Date dateNow = new Date(); fileDb.setDeleteDate(dateNow); + + // Create audit log + AuditLogUtil.create(fileDb, AuditLogType.DELETE); } /** @@ -113,6 +121,9 @@ public class FileDao { fileFromDb.setContent(file.getContent()); fileFromDb.setOrder(file.getOrder()); + // Create audit log + AuditLogUtil.create(fileFromDb, AuditLogType.UPDATE); + return file; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java index c2a66103..8345de17 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java @@ -1,15 +1,22 @@ package com.sismics.docs.core.dao.jpa; -import com.sismics.docs.core.dao.jpa.dto.TagDto; -import com.sismics.docs.core.dao.jpa.dto.TagStatDto; -import com.sismics.docs.core.model.jpa.DocumentTag; -import com.sismics.docs.core.model.jpa.Tag; -import com.sismics.util.context.ThreadLocalContext; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.UUID; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.Query; -import java.util.*; + +import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.dao.jpa.dto.TagDto; +import com.sismics.docs.core.dao.jpa.dto.TagStatDto; +import com.sismics.docs.core.model.jpa.DocumentTag; +import com.sismics.docs.core.model.jpa.Tag; +import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.util.context.ThreadLocalContext; /** * Tag DAO. @@ -153,6 +160,9 @@ public class TagDao { tag.setCreateDate(new Date()); em.persist(tag); + // Create audit log + AuditLogUtil.create(tag, AuditLogType.CREATE); + return tag.getId(); } @@ -213,6 +223,9 @@ public class TagDao { q = em.createQuery("delete DocumentTag dt where dt.tagId = :tagId"); q.setParameter("tagId", tagId); q.executeUpdate(); + + // Create audit log + AuditLogUtil.create(tagDb, AuditLogType.DELETE); } /** @@ -229,4 +242,28 @@ public class TagDao { q.setParameter("name", "%" + name + "%"); return q.getResultList(); } + + /** + * Update a tag. + * + * @param tag Tag to update + * @return Updated tag + */ + public Tag update(Tag tag) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the tag + Query q = em.createQuery("select t from Tag t where t.id = :id and t.deleteDate is null"); + q.setParameter("id", tag.getId()); + Tag tagFromDb = (Tag) q.getSingleResult(); + + // Update the tag + tagFromDb.setName(tag.getName()); + tagFromDb.setColor(tag.getColor()); + + // Create audit log + AuditLogUtil.create(tagFromDb, AuditLogType.UPDATE); + + return tagFromDb; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java index c332cf66..d9bc4c7e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java @@ -15,10 +15,12 @@ import javax.persistence.Query; import org.mindrot.jbcrypt.BCrypt; import com.google.common.base.Joiner; +import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.jpa.criteria.UserCriteria; import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.QueryParam; @@ -73,11 +75,15 @@ public class UserDao { throw new Exception("AlreadyExistingUsername"); } + // Create the user user.setCreateDate(new Date()); user.setPassword(hashPassword(user.getPassword())); user.setTheme(Constants.DEFAULT_THEME_ID); em.persist(user); + // Create audit log + AuditLogUtil.create(user, AuditLogType.CREATE); + return user.getId(); } @@ -101,6 +107,9 @@ public class UserDao { userFromDb.setTheme(user.getTheme()); userFromDb.setFirstConnection(user.isFirstConnection()); + // Create audit log + AuditLogUtil.create(userFromDb, AuditLogType.UPDATE); + return user; } @@ -121,6 +130,9 @@ public class UserDao { // Update the user userFromDb.setPassword(hashPassword(user.getPassword())); + // Create audit log + AuditLogUtil.create(userFromDb, AuditLogType.UPDATE); + return user; } @@ -194,6 +206,9 @@ public class UserDao { q = em.createQuery("delete from AuthenticationToken at where at.userId = :userId"); q.setParameter("userId", userFromDb.getId()); q.executeUpdate(); + + // Create audit log + AuditLogUtil.create(userFromDb, AuditLogType.DELETE); } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/AuditLogCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/AuditLogCriteria.java new file mode 100644 index 00000000..6c209d22 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/AuditLogCriteria.java @@ -0,0 +1,36 @@ +package com.sismics.docs.core.dao.jpa.criteria; + + + +/** + * Audit log criteria. + * + * @author bgamard + */ +public class AuditLogCriteria { + /** + * Document ID. + */ + private String documentId; + + /** + * User ID. + */ + private String userId; + + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AuditLogDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AuditLogDto.java new file mode 100644 index 00000000..44e4e920 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AuditLogDto.java @@ -0,0 +1,91 @@ +package com.sismics.docs.core.dao.jpa.dto; + +import javax.persistence.Id; + +import com.sismics.docs.core.constant.AuditLogType; + +/** + * Audit log DTO. + * + * @author bgamard + */ +public class AuditLogDto { + /** + * Audit log ID. + */ + @Id + private String id; + + /** + * Entity ID. + */ + private String entityId; + + /** + * Entity class. + */ + private String entityClass; + + /** + * Audit log type. + */ + private AuditLogType type; + + /** + * Audit log message. + */ + private String message; + + /** + * Creation date. + */ + private Long createTimestamp; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public String getEntityClass() { + return entityClass; + } + + public void setEntityClass(String entityClass) { + this.entityClass = entityClass; + } + + public AuditLogType getType() { + return type; + } + + public void setType(AuditLogType type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Long getCreateTimestamp() { + return createTimestamp; + } + + public void setCreateTimestamp(Long createTimestamp) { + this.createTimestamp = createTimestamp; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java index dc846c92..ab31e2b1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java @@ -4,6 +4,7 @@ import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; @@ -11,6 +12,7 @@ import javax.persistence.Table; import com.google.common.base.Objects; import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.util.AuditLogUtil; /** * ACL entity. @@ -18,8 +20,9 @@ import com.sismics.docs.core.constant.PermType; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_ACL") -public class Acl { +public class Acl implements Loggable { /** * ACL ID. */ @@ -84,6 +87,7 @@ public class Acl { this.targetId = targetId; } + @Override public Date getDeleteDate() { return deleteDate; } @@ -101,4 +105,9 @@ public class Acl { .add("targetId", targetId) .toString(); } + + @Override + public String toMessage() { + return perm.name(); + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java new file mode 100644 index 00000000..3f68ceb7 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java @@ -0,0 +1,178 @@ +package com.sismics.docs.core.model.jpa; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.google.common.base.Objects; +import com.sismics.docs.core.constant.AuditLogType; + +/** + * Audit log. + * + * @author bgamard + */ +@Entity +@Table(name = "T_AUDIT_LOG") +public class AuditLog { + /** + * Audit log ID. + */ + @Id + @Column(name = "LOG_ID_C", length = 36) + private String id; + + /** + * Entity ID. + */ + @Column(name = "LOG_IDENTITY_C", nullable = false, length = 36) + private String entityId; + + /** + * Entity class. + */ + @Column(name = "LOG_CLASSENTITY_C", nullable = false, length = 50) + private String entityClass; + + /** + * Audit log type. + */ + @Column(name = "LOG_TYPE_C", nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private AuditLogType type; + + /** + * Audit log message. + */ + @Column(name = "LOG_MESSAGE_C", length = 1000) + private String message; + + /** + * Creation date. + */ + @Column(name = "LOG_CREATEDATE_D", nullable = false) + private Date createDate; + + /** + * Getter of id. + * + * @return id + */ + public String getId() { + return id; + } + + /** + * Setter of id. + * + * @param id id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Getter of entityId. + * + * @return entityId + */ + public String getEntityId() { + return entityId; + } + + /** + * Setter of entityId. + * + * @param entityId entityId + */ + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + /** + * Getter of entityClass. + * + * @return entityClass + */ + public String getEntityClass() { + return entityClass; + } + + /** + * Setter of entityClass. + * + * @param entityClass entityClass + */ + public void setEntityClass(String entityClass) { + this.entityClass = entityClass; + } + + /** + * Getter of message. + * + * @return message + */ + public String getMessage() { + return message; + } + + /** + * Setter of message. + * + * @param message message + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Getter of type. + * + * @return type + */ + public AuditLogType getType() { + return type; + } + + /** + * Setter of type. + * + * @param type type + */ + public void setType(AuditLogType type) { + this.type = type; + } + + /** + * Getter of createDate. + * + * @return createDate + */ + public Date getCreateDate() { + return createDate; + } + + /** + * Setter of createDate. + * + * @param createDate createDate + */ + public void setCreateDate(Date createDate) { + this.createDate = createDate; + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("id", id) + .add("entityId", entityId) + .add("entityClass", entityClass) + .add("type", type) + .toString(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java index 4622f4fe..c376767c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java @@ -1,11 +1,14 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; + import java.util.Date; /** @@ -14,8 +17,9 @@ import java.util.Date; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_DOCUMENT") -public class Document { +public class Document implements Loggable { /** * Document ID. */ @@ -172,6 +176,7 @@ public class Document { * * @return the deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -191,4 +196,9 @@ public class Document { .add("id", id) .toString(); } + + @Override + public String toMessage() { + return title; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java index 5ddd6641..673d4995 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java @@ -1,12 +1,15 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Lob; import javax.persistence.Table; + import java.util.Date; /** @@ -15,8 +18,9 @@ import java.util.Date; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_FILE") -public class File { +public class File implements Loggable { /** * File ID. */ @@ -144,6 +148,7 @@ public class File { * * @return the deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -217,4 +222,9 @@ public class File { .add("id", id) .toString(); } + + @Override + public String toMessage() { + return documentId; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Loggable.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Loggable.java new file mode 100644 index 00000000..3b7e1837 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Loggable.java @@ -0,0 +1,25 @@ +package com.sismics.docs.core.model.jpa; + +import java.util.Date; + +/** + * An entity which can be logged. + * + * @author bgamard + */ +public interface Loggable { + /** + * Get a string representation of this entity for logging purpose. + * Avoid returning sensitive data like passwords. + * + * @return Entity message + */ + public String toMessage(); + + /** + * Loggable are soft deletable. + * + * @return deleteDate + */ + public Date getDeleteDate(); +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java index b9e48674..c49cded3 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java @@ -1,11 +1,14 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; + import java.util.Date; /** @@ -14,8 +17,9 @@ import java.util.Date; * @author bgamard */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_TAG") -public class Tag { +public class Tag implements Loggable { /** * Tag ID. */ @@ -148,6 +152,7 @@ public class Tag { * * @return deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -168,4 +173,9 @@ public class Tag { .add("name", name) .toString(); } + + @Override + public String toMessage() { + return name; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java index 3a525ed5..0e6227d7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java @@ -1,12 +1,15 @@ package com.sismics.docs.core.model.jpa; -import com.google.common.base.Objects; +import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; -import java.util.Date; + +import com.google.common.base.Objects; +import com.sismics.docs.core.util.AuditLogUtil; /** * User entity. @@ -14,8 +17,9 @@ import java.util.Date; * @author jtremeaux */ @Entity +@EntityListeners(AuditLogUtil.class) @Table(name = "T_USER") -public class User { +public class User implements Loggable { /** * User ID. */ @@ -250,6 +254,7 @@ public class User { * * @return deleteDate */ + @Override public Date getDeleteDate() { return deleteDate; } @@ -286,4 +291,9 @@ public class User { .add("username", username) .toString(); } + + @Override + public String toMessage() { + return username; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java new file mode 100644 index 00000000..ffc3d089 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java @@ -0,0 +1,37 @@ +package com.sismics.docs.core.util; + +import javax.persistence.EntityManager; + +import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.dao.jpa.AuditLogDao; +import com.sismics.docs.core.model.jpa.AuditLog; +import com.sismics.docs.core.model.jpa.Loggable; +import com.sismics.util.context.ThreadLocalContext; + +/** + * Audit log utilities. + * + * @author bgamard + */ +public class AuditLogUtil { + /** + * Create an audit log. + * + * @param entity Entity + * @param type Audit log type + */ + public static void create(Loggable loggable, AuditLogType type) { + // Get the entity ID + EntityManager em = ThreadLocalContext.get().getEntityManager(); + String entityId = (String) em.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(loggable); + + // Create the audit log + AuditLogDao auditLogDao = new AuditLogDao(); + AuditLog auditLog = new AuditLog(); + auditLog.setEntityId(entityId); + auditLog.setEntityClass(loggable.getClass().getSimpleName()); + auditLog.setType(type); + auditLog.setMessage(loggable.toMessage()); + auditLogDao.create(auditLog); + } +} diff --git a/docs-core/src/main/resources/META-INF/persistence.xml b/docs-core/src/main/resources/META-INF/persistence.xml index 51ff2cc5..16f8eff0 100644 --- a/docs-core/src/main/resources/META-INF/persistence.xml +++ b/docs-core/src/main/resources/META-INF/persistence.xml @@ -17,5 +17,6 @@ com.sismics.docs.core.model.jpa.DocumentTag com.sismics.docs.core.model.jpa.Share com.sismics.docs.core.model.jpa.Acl + com.sismics.docs.core.model.jpa.AuditLog \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql index 78ee9cee..a3ecc9c4 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql @@ -1,4 +1,6 @@ alter table T_FILE alter column FIL_IDUSER_C set not null; alter table T_AUTHENTICATION_TOKEN add column AUT_IP_C varchar(45); alter table T_AUTHENTICATION_TOKEN add column AUT_UA_C varchar(1000); +create cached table T_AUDIT_LOG ( LOG_ID_C varchar(36) not null, LOG_IDENTITY_C varchar(36) not null, LOG_CLASSENTITY_C varchar(50) not null, LOG_TYPE_C varchar(50) not null, LOG_MESSAGE_C varchar(1000), LOG_CREATEDATE_D datetime, primary key (LOG_ID_C) ); +create index IDX_LOG_COMPOSITE on T_AUDIT_LOG (LOG_IDENTITY_C, LOG_CLASSENTITY_C); update T_CONFIG set CFG_VALUE_C='10' where CFG_ID_C='DB_VERSION'; \ No newline at end of file diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java index 6582c488..c10ab7fb 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java @@ -117,8 +117,6 @@ public class RequestContextFilter implements Filter { throw new ServletException(e); } } - - ThreadLocalContext.cleanup(); // No error processing the request : commit / rollback the current transaction depending on the HTTP code if (em.isOpen()) { @@ -143,5 +141,7 @@ public class RequestContextFilter implements Filter { } } } + + ThreadLocalContext.cleanup(); } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java index 4d94ac10..9243e718 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java @@ -20,17 +20,13 @@ import org.apache.log4j.Logger; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; -import com.sismics.docs.core.dao.jpa.DocumentDao; import com.sismics.docs.core.dao.jpa.FileDao; -import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; -import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.ConfigUtil; import com.sismics.docs.core.util.DirectoryUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; -import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; @@ -64,20 +60,6 @@ public class AppResource extends BaseResource { JSONObject response = new JSONObject(); - // Specific data - DocumentDao documentDao = new DocumentDao(); - PaginatedList paginatedList = PaginatedLists.create(1, 0); - SortCriteria sortCriteria = new SortCriteria(0, true); - DocumentCriteria documentCriteria = new DocumentCriteria(); - documentCriteria.setUserId(principal.getId()); - try { - documentDao.findByCriteria(paginatedList, documentCriteria, sortCriteria); - } catch (Exception e) { - throw new ServerException("SearchError", "Error searching in documents", e); - } - response.put("document_count", paginatedList.getResultCount()); - - // General data response.put("current_version", currentVersion.replace("-SNAPSHOT", "")); response.put("min_version", minVersion); response.put("total_memory", Runtime.getRuntime().totalMemory()); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java new file mode 100644 index 00000000..8a40472d --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java @@ -0,0 +1,90 @@ +package com.sismics.docs.rest.resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; + +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.AclDao; +import com.sismics.docs.core.dao.jpa.AuditLogDao; +import com.sismics.docs.core.dao.jpa.criteria.AuditLogCriteria; +import com.sismics.docs.core.dao.jpa.dto.AuditLogDto; +import com.sismics.docs.core.util.jpa.PaginatedList; +import com.sismics.docs.core.util.jpa.PaginatedLists; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.rest.exception.ForbiddenClientException; +import com.sismics.rest.exception.ServerException; + +/** + * Audit log REST resources. + * + * @author bgamard + */ +@Path("/auditlog") +public class AuditLogResource extends BaseResource { + /** + * Returns the list of all logs for a document or user. + * + * @return Response + * @throws JSONException + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response list(@QueryParam("document") String documentId) throws JSONException { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // On a document or a user? + PaginatedList paginatedList = PaginatedLists.create(100, 0); + SortCriteria sortCriteria = new SortCriteria(1, true); + AuditLogCriteria criteria = new AuditLogCriteria(); + if (documentId == null) { + // Search logs for a user + criteria.setUserId(principal.getId()); + } else { + // Check ACL on the document + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(documentId, PermType.READ, principal.getId())) { + throw new ForbiddenClientException(); + } + criteria.setDocumentId(documentId); + } + + // Search the logs + try { + AuditLogDao auditLogDao = new AuditLogDao(); + auditLogDao.findByCriteria(paginatedList, criteria, sortCriteria); + } catch (Exception e) { + throw new ServerException("SearchError", "Error searching in logs", e); + } + + // Assemble the results + List logs = new ArrayList<>(); + JSONObject response = new JSONObject(); + for (AuditLogDto auditLogDto : paginatedList.getResultList()) { + JSONObject log = new JSONObject(); + log.put("id", auditLogDto.getId()); + log.put("target", auditLogDto.getEntityId()); + log.put("class", auditLogDto.getEntityClass()); + log.put("type", auditLogDto.getType().name()); + log.put("message", auditLogDto.getMessage()); + log.put("create_date", auditLogDto.getCreateTimestamp()); + logs.add(log); + } + + // Send the response + response.put("logs", logs); + response.put("total", paginatedList.getResultCount()); + return Response.ok().entity(response).build(); + } +} diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 8e39e17c..c276e44a 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -447,6 +447,8 @@ public class DocumentResource extends BaseResource { document.setLanguage(language); } + document = documentDao.update(document); + // Update tags updateTagList(id, tagList); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java index 87fad66e..8e09de5d 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java @@ -175,6 +175,8 @@ public class TagResource extends BaseResource { tag.setColor(color); } + tagDao.update(tag); + JSONObject response = new JSONObject(); response.put("id", id); return Response.ok().entity(response).build(); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js index 2bdba230..e5433e09 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js @@ -9,6 +9,11 @@ angular.module('docs').controller('DocumentDefault', function($scope, $state, Re $scope.app = data; }); + // Load user audit log + Restangular.one('auditlog').get().then(function(data) { + $scope.logs = data.logs; + }); + /** * Load unlinked files. */ diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index e68adb58..ba3b8790 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -1,10 +1,6 @@
-

- {{ app.document_count }} document{{ app.document_count > 1 ? 's' : '' }} in the database -

-
@@ -45,6 +41,17 @@
+ + + + + + + + + +
DateMessage
{{ log.create_date | date: 'yyyy-MM-dd HH:mm' }}{{ log.class }} {{ log.type }} {{ log.message }}
+
  • Version: {{ app.current_version }}
  • diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index 3c79eb07..64e2e7fb 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -41,7 +41,6 @@ public class TestAppResource extends BaseJerseyTest { Assert.assertTrue(freeMemory > 0); Long totalMemory = json.getLong("total_memory"); Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory); - Assert.assertEquals(0, json.getInt("document_count")); // Rebuild Lucene index appResource = resource().path("/app/batch/reindex"); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java new file mode 100644 index 00000000..c94ad714 --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java @@ -0,0 +1,98 @@ +package com.sismics.docs.rest; + +import java.util.Date; + +import com.sismics.docs.rest.filter.CookieAuthenticationFilter; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.ClientResponse.Status; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.core.util.MultivaluedMapImpl; + +import junit.framework.Assert; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.junit.Test; + +/** + * Test the audit log resource. + * + * @author bgamard + */ +public class TestAuditLogResource extends BaseJerseyTest { + /** + * Test the audit log resource. + * + * @throws JSONException + */ + @Test + public void testAuditLogResource() throws JSONException { + // Login auditlog1 + clientUtil.createUser("auditlog1"); + String auditlog1Token = clientUtil.login("auditlog1"); + + // Create a tag + WebResource tagResource = resource().path("/tag"); + tagResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + MultivaluedMapImpl postParams = new MultivaluedMapImpl(); + postParams.add("name", "SuperTag"); + postParams.add("color", "#ffff00"); + ClientResponse response = tagResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + JSONObject json = response.getEntity(JSONObject.class); + String tag1Id = json.optString("id"); + Assert.assertNotNull(tag1Id); + + // Create a document + WebResource documentResource = resource().path("/document"); + documentResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("title", "My super title document 1"); + postParams.add("description", "My super description for document 1"); + postParams.add("tags", tag1Id); + postParams.add("language", "eng"); + long create1Date = new Date().getTime(); + postParams.add("create_date", create1Date); + response = documentResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + String document1Id = json.optString("id"); + Assert.assertNotNull(document1Id); + + // Get all logs for the document + WebResource auditLogResource = resource().path("/auditlog"); + auditLogResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = auditLogResource.queryParam("document", document1Id).get(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + JSONArray logs = json.getJSONArray("logs"); + Assert.assertTrue(logs.length() == 3); + + // Get all logs for the current user + auditLogResource = resource().path("/auditlog"); + auditLogResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = auditLogResource.get(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + logs = json.getJSONArray("logs"); + Assert.assertTrue(logs.length() == 3); + + // Deletes a tag + tagResource = resource().path("/tag/" + tag1Id); + tagResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = tagResource.delete(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + Assert.assertEquals("ok", json.getString("status")); + + // Get all logs for the current user + auditLogResource = resource().path("/auditlog"); + auditLogResource.addFilter(new CookieAuthenticationFilter(auditlog1Token)); + response = auditLogResource.get(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + logs = json.getJSONArray("logs"); + Assert.assertTrue(logs.length() == 4); + } +} \ No newline at end of file