#20: Audit log displayed on main screen

This commit is contained in:
jendib 2015-05-17 22:20:34 +02:00
parent b2a38cea62
commit ea4e3fd8f2
28 changed files with 890 additions and 39 deletions

View File

@ -0,0 +1,23 @@
package com.sismics.docs.core.constant;
/**
* Audit log types.
*
* @author bgamard
*/
public enum AuditLogType {
/**
* Create.
*/
CREATE,
/**
* Update.
*/
UPDATE,
/**
* Delete.
*/
DELETE
}

View File

@ -9,9 +9,11 @@ import javax.persistence.EntityManager;
import javax.persistence.Query; import javax.persistence.Query;
import com.sismics.docs.core.constant.AclTargetType; 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.constant.PermType;
import com.sismics.docs.core.dao.jpa.dto.AclDto; import com.sismics.docs.core.dao.jpa.dto.AclDto;
import com.sismics.docs.core.model.jpa.Acl; import com.sismics.docs.core.model.jpa.Acl;
import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.context.ThreadLocalContext;
/** /**
@ -35,6 +37,9 @@ public class AclDao {
EntityManager em = ThreadLocalContext.get().getEntityManager(); EntityManager em = ThreadLocalContext.get().getEntityManager();
em.persist(acl); em.persist(acl);
// Create audit log
AuditLogUtil.create(acl, AuditLogType.CREATE);
return acl.getId(); return acl.getId();
} }
@ -121,9 +126,22 @@ public class AclDao {
* @param perm Permission * @param perm Permission
* @param targetId Target ID * @param targetId Target ID
*/ */
@SuppressWarnings("unchecked")
public void delete(String sourceId, PermType perm, String targetId) { public void delete(String sourceId, PermType perm, String targetId) {
EntityManager em = ThreadLocalContext.get().getEntityManager(); 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<Acl> 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("sourceId", sourceId);
q.setParameter("perm", perm); q.setParameter("perm", perm);
q.setParameter("targetId", targetId); q.setParameter("targetId", targetId);

View File

@ -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<AuditLogDto> paginatedList, AuditLogCriteria criteria, SortCriteria sortCriteria) throws Exception {
Map<String, Object> parameterMap = new HashMap<String, Object>();
List<String> criteriaList = new ArrayList<String>();
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<Object[]> l = PaginatedLists.executePaginatedQuery(paginatedList, queryParam, sortCriteria);
// Assemble results
List<AuditLogDto> auditLogDtoList = new ArrayList<AuditLogDto>();
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);
}
}

View File

@ -15,11 +15,13 @@ import javax.persistence.Query;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Strings; 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.constant.PermType;
import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria;
import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.dao.jpa.dto.DocumentDto;
import com.sismics.docs.core.dao.lucene.LuceneDao; import com.sismics.docs.core.dao.lucene.LuceneDao;
import com.sismics.docs.core.model.jpa.Document; 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.PaginatedList;
import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.core.util.jpa.QueryParam; import com.sismics.docs.core.util.jpa.QueryParam;
@ -47,6 +49,9 @@ public class DocumentDao {
EntityManager em = ThreadLocalContext.get().getEntityManager(); EntityManager em = ThreadLocalContext.get().getEntityManager();
em.persist(document); em.persist(document);
// Create audit log
AuditLogUtil.create(document, AuditLogType.CREATE);
return document.getId(); return document.getId();
} }
@ -145,6 +150,9 @@ public class DocumentDao {
q.setParameter("documentId", id); q.setParameter("documentId", id);
q.setParameter("dateNow", dateNow); q.setParameter("dateNow", dateNow);
q.executeUpdate(); 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 paginatedList List of documents (updated by side effects)
* @param criteria Search criteria * @param criteria Search criteria
* @param sortCriteria Sort criteria
* @return List of documents * @return List of documents
* @throws Exception * @throws Exception
*/ */
@ -248,4 +257,30 @@ public class DocumentDao {
paginatedList.setResultList(documentDtoList); 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;
}
} }

View File

@ -8,7 +8,9 @@ import javax.persistence.EntityManager;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.persistence.Query; import javax.persistence.Query;
import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.context.ThreadLocalContext;
/** /**
@ -33,6 +35,9 @@ public class FileDao {
file.setCreateDate(new Date()); file.setCreateDate(new Date());
em.persist(file); em.persist(file);
// Create audit log
AuditLogUtil.create(file, AuditLogType.CREATE);
return file.getId(); return file.getId();
} }
@ -92,6 +97,9 @@ public class FileDao {
// Delete the file // Delete the file
Date dateNow = new Date(); Date dateNow = new Date();
fileDb.setDeleteDate(dateNow); fileDb.setDeleteDate(dateNow);
// Create audit log
AuditLogUtil.create(fileDb, AuditLogType.DELETE);
} }
/** /**
@ -113,6 +121,9 @@ public class FileDao {
fileFromDb.setContent(file.getContent()); fileFromDb.setContent(file.getContent());
fileFromDb.setOrder(file.getOrder()); fileFromDb.setOrder(file.getOrder());
// Create audit log
AuditLogUtil.create(fileFromDb, AuditLogType.UPDATE);
return file; return file;
} }

View File

@ -1,15 +1,22 @@
package com.sismics.docs.core.dao.jpa; package com.sismics.docs.core.dao.jpa;
import com.sismics.docs.core.dao.jpa.dto.TagDto; import java.util.ArrayList;
import com.sismics.docs.core.dao.jpa.dto.TagStatDto; import java.util.Date;
import com.sismics.docs.core.model.jpa.DocumentTag; import java.util.List;
import com.sismics.docs.core.model.jpa.Tag; import java.util.Set;
import com.sismics.util.context.ThreadLocalContext; import java.util.UUID;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.persistence.Query; 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. * Tag DAO.
@ -153,6 +160,9 @@ public class TagDao {
tag.setCreateDate(new Date()); tag.setCreateDate(new Date());
em.persist(tag); em.persist(tag);
// Create audit log
AuditLogUtil.create(tag, AuditLogType.CREATE);
return tag.getId(); return tag.getId();
} }
@ -213,6 +223,9 @@ public class TagDao {
q = em.createQuery("delete DocumentTag dt where dt.tagId = :tagId"); q = em.createQuery("delete DocumentTag dt where dt.tagId = :tagId");
q.setParameter("tagId", tagId); q.setParameter("tagId", tagId);
q.executeUpdate(); q.executeUpdate();
// Create audit log
AuditLogUtil.create(tagDb, AuditLogType.DELETE);
} }
/** /**
@ -229,4 +242,28 @@ public class TagDao {
q.setParameter("name", "%" + name + "%"); q.setParameter("name", "%" + name + "%");
return q.getResultList(); 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;
}
} }

View File

@ -15,10 +15,12 @@ import javax.persistence.Query;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
import com.google.common.base.Joiner; 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.constant.Constants;
import com.sismics.docs.core.dao.jpa.criteria.UserCriteria; import com.sismics.docs.core.dao.jpa.criteria.UserCriteria;
import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.dao.jpa.dto.UserDto;
import com.sismics.docs.core.model.jpa.User; 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.PaginatedList;
import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.core.util.jpa.QueryParam; import com.sismics.docs.core.util.jpa.QueryParam;
@ -73,11 +75,15 @@ public class UserDao {
throw new Exception("AlreadyExistingUsername"); throw new Exception("AlreadyExistingUsername");
} }
// Create the user
user.setCreateDate(new Date()); user.setCreateDate(new Date());
user.setPassword(hashPassword(user.getPassword())); user.setPassword(hashPassword(user.getPassword()));
user.setTheme(Constants.DEFAULT_THEME_ID); user.setTheme(Constants.DEFAULT_THEME_ID);
em.persist(user); em.persist(user);
// Create audit log
AuditLogUtil.create(user, AuditLogType.CREATE);
return user.getId(); return user.getId();
} }
@ -101,6 +107,9 @@ public class UserDao {
userFromDb.setTheme(user.getTheme()); userFromDb.setTheme(user.getTheme());
userFromDb.setFirstConnection(user.isFirstConnection()); userFromDb.setFirstConnection(user.isFirstConnection());
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE);
return user; return user;
} }
@ -121,6 +130,9 @@ public class UserDao {
// Update the user // Update the user
userFromDb.setPassword(hashPassword(user.getPassword())); userFromDb.setPassword(hashPassword(user.getPassword()));
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE);
return user; return user;
} }
@ -194,6 +206,9 @@ public class UserDao {
q = em.createQuery("delete from AuthenticationToken at where at.userId = :userId"); q = em.createQuery("delete from AuthenticationToken at where at.userId = :userId");
q.setParameter("userId", userFromDb.getId()); q.setParameter("userId", userFromDb.getId());
q.executeUpdate(); q.executeUpdate();
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.DELETE);
} }
/** /**

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -4,6 +4,7 @@ import java.util.Date;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EnumType; import javax.persistence.EnumType;
import javax.persistence.Enumerated; import javax.persistence.Enumerated;
import javax.persistence.Id; import javax.persistence.Id;
@ -11,6 +12,7 @@ import javax.persistence.Table;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.util.AuditLogUtil;
/** /**
* ACL entity. * ACL entity.
@ -18,8 +20,9 @@ import com.sismics.docs.core.constant.PermType;
* @author bgamard * @author bgamard
*/ */
@Entity @Entity
@EntityListeners(AuditLogUtil.class)
@Table(name = "T_ACL") @Table(name = "T_ACL")
public class Acl { public class Acl implements Loggable {
/** /**
* ACL ID. * ACL ID.
*/ */
@ -84,6 +87,7 @@ public class Acl {
this.targetId = targetId; this.targetId = targetId;
} }
@Override
public Date getDeleteDate() { public Date getDeleteDate() {
return deleteDate; return deleteDate;
} }
@ -101,4 +105,9 @@ public class Acl {
.add("targetId", targetId) .add("targetId", targetId)
.toString(); .toString();
} }
@Override
public String toMessage() {
return perm.name();
}
} }

View File

@ -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();
}
}

View File

@ -1,11 +1,14 @@
package com.sismics.docs.core.model.jpa; package com.sismics.docs.core.model.jpa;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.sismics.docs.core.util.AuditLogUtil;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.Date; import java.util.Date;
/** /**
@ -14,8 +17,9 @@ import java.util.Date;
* @author bgamard * @author bgamard
*/ */
@Entity @Entity
@EntityListeners(AuditLogUtil.class)
@Table(name = "T_DOCUMENT") @Table(name = "T_DOCUMENT")
public class Document { public class Document implements Loggable {
/** /**
* Document ID. * Document ID.
*/ */
@ -172,6 +176,7 @@ public class Document {
* *
* @return the deleteDate * @return the deleteDate
*/ */
@Override
public Date getDeleteDate() { public Date getDeleteDate() {
return deleteDate; return deleteDate;
} }
@ -191,4 +196,9 @@ public class Document {
.add("id", id) .add("id", id)
.toString(); .toString();
} }
@Override
public String toMessage() {
return title;
}
} }

View File

@ -1,12 +1,15 @@
package com.sismics.docs.core.model.jpa; package com.sismics.docs.core.model.jpa;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.sismics.docs.core.util.AuditLogUtil;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Lob; import javax.persistence.Lob;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.Date; import java.util.Date;
/** /**
@ -15,8 +18,9 @@ import java.util.Date;
* @author bgamard * @author bgamard
*/ */
@Entity @Entity
@EntityListeners(AuditLogUtil.class)
@Table(name = "T_FILE") @Table(name = "T_FILE")
public class File { public class File implements Loggable {
/** /**
* File ID. * File ID.
*/ */
@ -144,6 +148,7 @@ public class File {
* *
* @return the deleteDate * @return the deleteDate
*/ */
@Override
public Date getDeleteDate() { public Date getDeleteDate() {
return deleteDate; return deleteDate;
} }
@ -217,4 +222,9 @@ public class File {
.add("id", id) .add("id", id)
.toString(); .toString();
} }
@Override
public String toMessage() {
return documentId;
}
} }

View File

@ -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();
}

View File

@ -1,11 +1,14 @@
package com.sismics.docs.core.model.jpa; package com.sismics.docs.core.model.jpa;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.sismics.docs.core.util.AuditLogUtil;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.Date; import java.util.Date;
/** /**
@ -14,8 +17,9 @@ import java.util.Date;
* @author bgamard * @author bgamard
*/ */
@Entity @Entity
@EntityListeners(AuditLogUtil.class)
@Table(name = "T_TAG") @Table(name = "T_TAG")
public class Tag { public class Tag implements Loggable {
/** /**
* Tag ID. * Tag ID.
*/ */
@ -148,6 +152,7 @@ public class Tag {
* *
* @return deleteDate * @return deleteDate
*/ */
@Override
public Date getDeleteDate() { public Date getDeleteDate() {
return deleteDate; return deleteDate;
} }
@ -168,4 +173,9 @@ public class Tag {
.add("name", name) .add("name", name)
.toString(); .toString();
} }
@Override
public String toMessage() {
return name;
}
} }

View File

@ -1,12 +1,15 @@
package com.sismics.docs.core.model.jpa; package com.sismics.docs.core.model.jpa;
import com.google.common.base.Objects; import java.util.Date;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.Date;
import com.google.common.base.Objects;
import com.sismics.docs.core.util.AuditLogUtil;
/** /**
* User entity. * User entity.
@ -14,8 +17,9 @@ import java.util.Date;
* @author jtremeaux * @author jtremeaux
*/ */
@Entity @Entity
@EntityListeners(AuditLogUtil.class)
@Table(name = "T_USER") @Table(name = "T_USER")
public class User { public class User implements Loggable {
/** /**
* User ID. * User ID.
*/ */
@ -250,6 +254,7 @@ public class User {
* *
* @return deleteDate * @return deleteDate
*/ */
@Override
public Date getDeleteDate() { public Date getDeleteDate() {
return deleteDate; return deleteDate;
} }
@ -286,4 +291,9 @@ public class User {
.add("username", username) .add("username", username)
.toString(); .toString();
} }
@Override
public String toMessage() {
return username;
}
} }

View File

@ -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);
}
}

View File

@ -17,5 +17,6 @@
<class>com.sismics.docs.core.model.jpa.DocumentTag</class> <class>com.sismics.docs.core.model.jpa.DocumentTag</class>
<class>com.sismics.docs.core.model.jpa.Share</class> <class>com.sismics.docs.core.model.jpa.Share</class>
<class>com.sismics.docs.core.model.jpa.Acl</class> <class>com.sismics.docs.core.model.jpa.Acl</class>
<class>com.sismics.docs.core.model.jpa.AuditLog</class>
</persistence-unit> </persistence-unit>
</persistence> </persistence>

View File

@ -1,4 +1,6 @@
alter table T_FILE alter column FIL_IDUSER_C set not null; 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_IP_C varchar(45);
alter table T_AUTHENTICATION_TOKEN add column AUT_UA_C varchar(1000); 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'; update T_CONFIG set CFG_VALUE_C='10' where CFG_ID_C='DB_VERSION';

View File

@ -118,8 +118,6 @@ public class RequestContextFilter implements Filter {
} }
} }
ThreadLocalContext.cleanup();
// No error processing the request : commit / rollback the current transaction depending on the HTTP code // No error processing the request : commit / rollback the current transaction depending on the HTTP code
if (em.isOpen()) { if (em.isOpen()) {
if (em.getTransaction() != null && em.getTransaction().isActive()) { if (em.getTransaction() != null && em.getTransaction().isActive()) {
@ -143,5 +141,7 @@ public class RequestContextFilter implements Filter {
} }
} }
} }
ThreadLocalContext.cleanup();
} }
} }

View File

@ -20,17 +20,13 @@ import org.apache.log4j.Logger;
import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject; 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.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.context.AppContext;
import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.ConfigUtil; import com.sismics.docs.core.util.ConfigUtil;
import com.sismics.docs.core.util.DirectoryUtil; import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedList;
import com.sismics.docs.core.util.jpa.PaginatedLists; 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.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException; import com.sismics.rest.exception.ServerException;
@ -64,20 +60,6 @@ public class AppResource extends BaseResource {
JSONObject response = new JSONObject(); JSONObject response = new JSONObject();
// Specific data
DocumentDao documentDao = new DocumentDao();
PaginatedList<DocumentDto> 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("current_version", currentVersion.replace("-SNAPSHOT", ""));
response.put("min_version", minVersion); response.put("min_version", minVersion);
response.put("total_memory", Runtime.getRuntime().totalMemory()); response.put("total_memory", Runtime.getRuntime().totalMemory());

View File

@ -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<AuditLogDto> 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<JSONObject> 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();
}
}

View File

@ -447,6 +447,8 @@ public class DocumentResource extends BaseResource {
document.setLanguage(language); document.setLanguage(language);
} }
document = documentDao.update(document);
// Update tags // Update tags
updateTagList(id, tagList); updateTagList(id, tagList);

View File

@ -175,6 +175,8 @@ public class TagResource extends BaseResource {
tag.setColor(color); tag.setColor(color);
} }
tagDao.update(tag);
JSONObject response = new JSONObject(); JSONObject response = new JSONObject();
response.put("id", id); response.put("id", id);
return Response.ok().entity(response).build(); return Response.ok().entity(response).build();

View File

@ -9,6 +9,11 @@ angular.module('docs').controller('DocumentDefault', function($scope, $state, Re
$scope.app = data; $scope.app = data;
}); });
// Load user audit log
Restangular.one('auditlog').get().then(function(data) {
$scope.logs = data.logs;
});
/** /**
* Load unlinked files. * Load unlinked files.
*/ */

View File

@ -1,10 +1,6 @@
<img src="img/loader.gif" ng-show="!app" /> <img src="img/loader.gif" ng-show="!app" />
<div ng-show="app"> <div ng-show="app">
<h1>
{{ app.document_count }} <small>document{{ app.document_count > 1 ? 's' : '' }} in the database</small>
</h1>
<div class="row upload-zone" ng-model="dropFiles" ng-file-drop drag-over-class="bg-success" <div class="row upload-zone" ng-model="dropFiles" ng-file-drop drag-over-class="bg-success"
ng-multiple="true" allow-dir="false" accept="image/*,application/pdf,application/zip" ng-file-change="fileDropped($files, $event, $rejectedFiles)"> ng-multiple="true" allow-dir="false" accept="image/*,application/pdf,application/zip" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files"> <div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files">
@ -45,6 +41,17 @@
<div ui-view="file"></div> <div ui-view="file"></div>
<table class="table">
<tr>
<th>Date</th>
<th>Message</th>
</tr>
<tr ng-repeat="log in logs">
<td>{{ log.create_date | date: 'yyyy-MM-dd HH:mm' }}</td>
<td>{{ log.class }} {{ log.type }} {{ log.message }}</td>
</tr>
</table>
<div class="text-muted text-right"> <div class="text-muted text-right">
<ul class="list-inline"> <ul class="list-inline">
<li><strong>Version:</strong> {{ app.current_version }}</li> <li><strong>Version:</strong> {{ app.current_version }}</li>

View File

@ -41,7 +41,6 @@ public class TestAppResource extends BaseJerseyTest {
Assert.assertTrue(freeMemory > 0); Assert.assertTrue(freeMemory > 0);
Long totalMemory = json.getLong("total_memory"); Long totalMemory = json.getLong("total_memory");
Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory); Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory);
Assert.assertEquals(0, json.getInt("document_count"));
// Rebuild Lucene index // Rebuild Lucene index
appResource = resource().path("/app/batch/reindex"); appResource = resource().path("/app/batch/reindex");

View File

@ -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);
}
}