diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/AclTargetType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/AclTargetType.java new file mode 100644 index 00000000..a101c0ae --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/AclTargetType.java @@ -0,0 +1,23 @@ +package com.sismics.docs.core.constant; + +/** + * ACL target types. + * + * @author bgamard + */ +public enum AclTargetType { + /** + * An user. + */ + USER, + + /** + * A group. + */ + GROUP, + + /** + * A share. + */ + SHARE +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/PermType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/PermType.java new file mode 100644 index 00000000..1103d5b5 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/PermType.java @@ -0,0 +1,18 @@ +package com.sismics.docs.core.constant; + +/** + * Permissions. + * + * @author bgamard + */ +public enum PermType { + /** + * Read document. + */ + READ, + + /** + * Write document. + */ + WRITE +} 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 new file mode 100644 index 00000000..97570e47 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java @@ -0,0 +1,133 @@ +package com.sismics.docs.core.dao.jpa; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +import com.sismics.docs.core.constant.AclTargetType; +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.util.context.ThreadLocalContext; + +/** + * ACL DAO. + * + * @author bgamard + */ +public class AclDao { + /** + * Creates a new ACL. + * + * @param acl ACL + * @return New ID + * @throws Exception + */ + public String create(Acl acl) { + // Create the UUID + acl.setId(UUID.randomUUID().toString()); + + // Create the ACL + EntityManager em = ThreadLocalContext.get().getEntityManager(); + em.persist(acl); + + return acl.getId(); + } + + /** + * Search ACLs by target ID. + * + * @param targetId Target ID + * @return ACL list + */ + @SuppressWarnings("unchecked") + public List getByTargetId(String targetId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + Query q = em.createQuery("select a from Acl a where a.targetId = :targetId and a.deleteDate is null"); + q.setParameter("targetId", targetId); + + return q.getResultList(); + } + + /** + * Search ACLs by source ID. + * + * @param sourceId Source ID + * @return ACL DTO list + */ + @SuppressWarnings("unchecked") + public List getBySourceId(String sourceId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + StringBuilder sb = new StringBuilder("select a.ACL_ID_C, a.ACL_PERM_C, a.ACL_TARGETID_C, u.USE_USERNAME_C, s.SHA_NAME_C"); + sb.append(" from T_ACL a "); + sb.append(" left join T_USER u on u.USE_ID_C = a.ACL_TARGETID_C "); + sb.append(" left join T_SHARE s on s.SHA_ID_C = a.ACL_TARGETID_C "); + sb.append(" where a.ACL_DELETEDATE_D is null and a.ACL_SOURCEID_C = :sourceId "); + + // Perform the query + Query q = em.createNativeQuery(sb.toString()); + q.setParameter("sourceId", sourceId); + List l = q.getResultList(); + + // Assemble results + List aclDtoList = new ArrayList(); + for (Object[] o : l) { + int i = 0; + AclDto aclDto = new AclDto(); + aclDto.setId((String) o[i++]); + aclDto.setPerm(PermType.valueOf((String) o[i++])); + aclDto.setTargetId((String) o[i++]); + String userName = (String) o[i++]; + String shareName = (String) o[i++]; + aclDto.setTargetName(userName == null ? shareName : userName); + aclDto.setTargetType(userName == null ? + AclTargetType.SHARE.name() : AclTargetType.USER.name()); + aclDtoList.add(aclDto); + } + return aclDtoList; + } + + /** + * Check if a source is accessible to a target. + * + * @param sourceId ACL source entity ID + * @parm perm Necessary permission + * @param targetId ACL target entity ID + * @return True if the document is accessible + */ + public boolean checkPermission(String sourceId, PermType perm, String targetId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + Query q = em.createQuery("select a from Acl a where a.sourceId = :sourceId and a.perm = :perm and a.targetId = :targetId and a.deleteDate is null"); + q.setParameter("sourceId", sourceId); + q.setParameter("perm", perm); + q.setParameter("targetId", targetId); + + // We have a matching permission + if (q.getResultList().size() > 0) { + return true; + } + + return false; + } + + /** + * Delete an ACL. + * + * @param sourceId Source ID + * @param perm Permission + * @param targetId Target ID + */ + 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"); + q.setParameter("sourceId", sourceId); + q.setParameter("perm", perm); + q.setParameter("targetId", targetId); + q.setParameter("dateNow", new Date()); + q.executeUpdate(); + } +} 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 de0cbcc7..b6c1bb14 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,6 +15,7 @@ import javax.persistence.Query; import com.google.common.base.Joiner; import com.google.common.base.Strings; +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; @@ -78,13 +79,17 @@ public class DocumentDao { * Returns an active document. * * @param id Document ID + * @param perm Permission needed * @param userId User ID * @return Document */ - public Document getDocument(String id, String userId) { + public Document getDocument(String id, PermType perm, String userId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select d from Document d where d.id = :id and d.userId = :userId and d.deleteDate is null"); + Query q = em.createNativeQuery("select d.* from T_DOCUMENT d " + + " join T_ACL a on a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_TARGETID_C = :userId and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null " + + " where d.DOC_ID_C = :id and d.DOC_DELETEDATE_D is null", Document.class); q.setParameter("id", id); + q.setParameter("perm", perm.name()); q.setParameter("userId", userId); return (Document) q.getSingleResult(); } @@ -112,7 +117,13 @@ public class DocumentDao { q.setParameter("dateNow", dateNow); q.executeUpdate(); - q = em.createQuery("update Share s set s.deleteDate = :dateNow where s.documentId = :documentId and s.deleteDate is null"); + // TODO Delete share from deleted ACLs +// q = em.createQuery("update Share s set s.deleteDate = :dateNow where s.documentId = :documentId and s.deleteDate is null"); +// q.setParameter("documentId", id); +// q.setParameter("dateNow", dateNow); +// q.executeUpdate(); + + q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.sourceId = :documentId"); q.setParameter("documentId", id); q.setParameter("dateNow", dateNow); q.executeUpdate(); @@ -145,19 +156,20 @@ public class DocumentDao { Map parameterMap = new HashMap(); List criteriaList = new ArrayList(); - StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C c0, d.DOC_TITLE_C c1, d.DOC_DESCRIPTION_C c2, d.DOC_CREATEDATE_D c3, d.DOC_LANGUAGE_C c4, s.SHA_ID_C is not null c5, "); + StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C c0, d.DOC_TITLE_C c1, d.DOC_DESCRIPTION_C c2, d.DOC_CREATEDATE_D c3, d.DOC_LANGUAGE_C c4, "); + sb.append(" (select count(s.SHA_ID_C) from T_SHARE s, T_ACL ac where ac.ACL_SOURCEID_C = d.DOC_ID_C and ac.ACL_TARGETID_C = s.SHA_ID_C and ac.ACL_DELETEDATE_D is null and s.SHA_DELETEDATE_D is null) c5, "); sb.append(" (select count(f.FIL_ID_C) from T_FILE f where f.FIL_DELETEDATE_D is null and f.FIL_IDDOC_C = d.DOC_ID_C) c6 "); sb.append(" from T_DOCUMENT d "); - sb.append(" left join T_SHARE s on s.SHA_IDDOCUMENT_C = d.DOC_ID_C and s.SHA_DELETEDATE_D is null "); // Adds search criteria if (criteria.getUserId() != null) { - criteriaList.add("d.DOC_IDUSER_C = :userId"); + // Read permission is enough for searching + sb.append(" join T_ACL a on a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_TARGETID_C = :userId and a.ACL_PERM_C = 'READ' and a.ACL_DELETEDATE_D is null "); parameterMap.put("userId", criteria.getUserId()); } if (!Strings.isNullOrEmpty(criteria.getSearch()) || !Strings.isNullOrEmpty(criteria.getFullSearch())) { LuceneDao luceneDao = new LuceneDao(); - Set documentIdList = luceneDao.search(criteria.getUserId(), criteria.getSearch(), criteria.getFullSearch()); + Set documentIdList = luceneDao.search(criteria.getSearch(), criteria.getFullSearch()); if (documentIdList.size() == 0) { // If the search doesn't find any document, the request should return nothing documentIdList.add(UUID.randomUUID().toString()); @@ -183,7 +195,7 @@ public class DocumentDao { } } if (criteria.getShared() != null && criteria.getShared()) { - criteriaList.add("s.SHA_ID_C is not null"); + criteriaList.add("(select count(s.SHA_ID_C) from T_SHARE s, T_ACL ac where ac.ACL_SOURCEID_C = d.DOC_ID_C and ac.ACL_TARGETID_C = s.SHA_ID_C and ac.ACL_DELETEDATE_D is null and s.SHA_DELETEDATE_D is null) > 0"); } if (criteria.getLanguage() != null) { criteriaList.add("d.DOC_LANGUAGE_C = :language"); @@ -211,7 +223,7 @@ public class DocumentDao { documentDto.setDescription((String) o[i++]); documentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); documentDto.setLanguage((String) o[i++]); - documentDto.setShared((Boolean) o[i++]); + documentDto.setShared(((Number) o[i++]).intValue() > 0); documentDto.setFileCount(((Number) o[i++]).intValue()); documentDtoList.add(documentDto); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/ShareDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/ShareDao.java index 8ed8e6dc..8bd3963e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/ShareDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/ShareDao.java @@ -1,14 +1,11 @@ package com.sismics.docs.core.dao.jpa; import java.util.Date; -import java.util.List; import java.util.UUID; import javax.persistence.EntityManager; -import javax.persistence.NoResultException; import javax.persistence.Query; -import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.Share; import com.sismics.util.context.ThreadLocalContext; @@ -37,23 +34,6 @@ public class ShareDao { return share.getId(); } - /** - * Returns an active share. - * - * @param id Share ID - * @return Document - */ - public Share getShare(String id) { - EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select s from Share s where s.id = :id and s.deleteDate is null"); - q.setParameter("id", id); - try { - return (Share) q.getSingleResult(); - } catch (NoResultException e) { - return null; - } - } - /** * Deletes a share. * @@ -70,44 +50,11 @@ public class ShareDao { // Delete the share Date dateNow = new Date(); shareDb.setDeleteDate(dateNow); - } - - /** - * Get shares by document ID. - * - * @param documentId Document ID - * @return List of shares - */ - @SuppressWarnings("unchecked") - public List getByDocumentId(String documentId) { - EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select s from Share s where s.documentId = :documentId and s.deleteDate is null"); - q.setParameter("documentId", documentId); - return q.getResultList(); - } - - /** - * Check if a document is visible. - * - * @param document Document to check for visibility - * @param userId Optional user trying to access the document - * @param shareId Optional share to access the document - * @return True if the document is visible - */ - public boolean checkVisibility(Document document, String userId, String shareId) { - // The user owns the document - if (document.getUserId().equals(userId)) { - return true; - } - // The share is linked to the document - if (shareId != null) { - Share share = getShare(shareId); - if (share != null && share.getDocumentId().equals(document.getId())) { - return true; - } - } - - return false; + // Delete the linked ACL + q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.targetId = :targetId"); + q.setParameter("targetId", id); + q.setParameter("dateNow", dateNow); + q.executeUpdate(); } } 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 53d564fa..c332cf66 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 @@ -1,7 +1,22 @@ 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 javax.persistence.NoResultException; +import javax.persistence.Query; + +import org.mindrot.jbcrypt.BCrypt; + import com.google.common.base.Joiner; 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.jpa.PaginatedList; @@ -9,13 +24,6 @@ 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; -import org.mindrot.jbcrypt.BCrypt; - -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; -import java.sql.Timestamp; -import java.util.*; /** * User DAO. @@ -204,13 +212,19 @@ public class UserDao { * @param paginatedList List of users (updated by side effects) * @param sortCriteria Sort criteria */ - public void findAll(PaginatedList paginatedList, SortCriteria sortCriteria) { + public void findByCriteria(PaginatedList paginatedList, UserCriteria criteria, SortCriteria sortCriteria) { Map parameterMap = new HashMap(); + List criteriaList = new ArrayList(); + StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_IDLOCALE_C as c4"); sb.append(" from T_USER u "); // Add search criterias - List criteriaList = new ArrayList(); + if (criteria.getSearch() != null) { + criteriaList.add("lower(u.USE_USERNAME_C) like lower(:search)"); + parameterMap.put("search", "%" + criteria.getSearch() + "%"); + } + criteriaList.add("u.USE_DELETEDATE_D is null"); if (!criteriaList.isEmpty()) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/UserCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/UserCriteria.java new file mode 100644 index 00000000..eff5e8b9 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/UserCriteria.java @@ -0,0 +1,34 @@ +package com.sismics.docs.core.dao.jpa.criteria; + + + +/** + * User criteria. + * + * @author bgamard + */ +public class UserCriteria { + /** + * Search query. + */ + private String search; + + /** + * Getter of search. + * + * @return the search + */ + public String getSearch() { + return search; + } + + /** + * Setter of search. + * + * @param search search + */ + public UserCriteria setSearch(String search) { + this.search = search; + return this; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AclDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AclDto.java new file mode 100644 index 00000000..85051a38 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/AclDto.java @@ -0,0 +1,91 @@ +package com.sismics.docs.core.dao.jpa.dto; + +import javax.persistence.Id; + +import com.sismics.docs.core.constant.PermType; + +/** + * Acl DTO. + * + * @author bgamard + */ +public class AclDto { + /** + * Acl ID. + */ + @Id + private String id; + + /** + * Target name. + */ + private String targetName; + + /** + * Permission. + */ + private PermType perm; + + /** + * Source ID. + */ + private String sourceId; + + /** + * Target ID. + */ + private String targetId; + + /** + * Target type. + */ + private String targetType; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTargetName() { + return targetName; + } + + public void setTargetName(String targetName) { + this.targetName = targetName; + } + + public PermType getPerm() { + return perm; + } + + public void setPerm(PermType perm) { + this.perm = perm; + } + + public String getSourceId() { + return sourceId; + } + + public void setSourceId(String sourceId) { + this.sourceId = sourceId; + } + + public String getTargetId() { + return targetId; + } + + public void setTargetId(String targetId) { + this.targetId = targetId; + } + + public String getTargetType() { + return targetType; + } + + public void setTargetType(String targetType) { + this.targetType = targetType; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/lucene/LuceneDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/lucene/LuceneDao.java index 474b7dcb..c76113c7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/lucene/LuceneDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/lucene/LuceneDao.java @@ -1,6 +1,5 @@ package com.sismics.docs.core.dao.lucene; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -13,7 +12,6 @@ import org.apache.lucene.document.TextField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.Term; -import org.apache.lucene.queries.TermsFilter; import org.apache.lucene.queryparser.flexible.standard.QueryParserUtil; import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser; import org.apache.lucene.search.BooleanClause.Occur; @@ -143,13 +141,12 @@ public class LuceneDao { /** * Search files. * - * @param userId User ID to filter on * @param searchQuery Search query on title and description * @param fullSearchQuery Search query on all fields * @return List of document IDs * @throws Exception */ - public Set search(String userId, String searchQuery, String fullSearchQuery) throws Exception { + public Set search(String searchQuery, String fullSearchQuery) throws Exception { // Escape query and add quotes so QueryParser generate a PhraseQuery searchQuery = "\"" + QueryParserUtil.escape(searchQuery + " " + fullSearchQuery) + "\""; fullSearchQuery = "\"" + QueryParserUtil.escape(fullSearchQuery) + "\""; @@ -164,13 +161,6 @@ public class LuceneDao { query.add(qpHelper.parse(searchQuery, "description"), Occur.SHOULD); query.add(qpHelper.parse(fullSearchQuery, "content"), Occur.SHOULD); - // Filter on provided user ID - List terms = new ArrayList(); - if (userId != null) { - terms.add(new Term("user_id", userId)); - } - TermsFilter userFilter = new TermsFilter(terms); - // Search DirectoryReader directoryReader = AppContext.getInstance().getIndexingService().getDirectoryReader(); Set documentIdList = new HashSet(); @@ -179,7 +169,7 @@ public class LuceneDao { return documentIdList; } IndexSearcher searcher = new IndexSearcher(directoryReader); - TopDocs topDocs = searcher.search(query, userFilter, Integer.MAX_VALUE); + TopDocs topDocs = searcher.search(query, Integer.MAX_VALUE); ScoreDoc[] docs = topDocs.scoreDocs; // Extract document IDs @@ -207,7 +197,6 @@ public class LuceneDao { private org.apache.lucene.document.Document getDocumentFromDocument(Document document) { org.apache.lucene.document.Document luceneDocument = new org.apache.lucene.document.Document(); luceneDocument.add(new StringField("id", document.getId(), Field.Store.YES)); - luceneDocument.add(new StringField("user_id", document.getUserId(), Field.Store.YES)); luceneDocument.add(new StringField("type", "document", Field.Store.YES)); if (document.getTitle() != null) { luceneDocument.add(new TextField("title", document.getTitle(), Field.Store.NO)); @@ -229,7 +218,6 @@ public class LuceneDao { private org.apache.lucene.document.Document getDocumentFromFile(File file, Document document) { org.apache.lucene.document.Document luceneDocument = new org.apache.lucene.document.Document(); luceneDocument.add(new StringField("id", file.getId(), Field.Store.YES)); - luceneDocument.add(new StringField("user_id", document.getUserId(), Field.Store.YES)); luceneDocument.add(new StringField("type", "file", Field.Store.YES)); luceneDocument.add(new StringField("document_id", file.getDocumentId(), Field.Store.YES)); if (file.getContent() != null) { 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 new file mode 100644 index 00000000..dc846c92 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java @@ -0,0 +1,104 @@ +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.PermType; + +/** + * ACL entity. + * + * @author bgamard + */ +@Entity +@Table(name = "T_ACL") +public class Acl { + /** + * ACL ID. + */ + @Id + @Column(name = "ACL_ID_C", length = 36) + private String id; + + /** + * ACL permission. + */ + @Column(name = "ACL_PERM_C", length = 30) + @Enumerated(EnumType.STRING) + private PermType perm; + + /** + * ACL source ID. + */ + @Column(name = "ACL_SOURCEID_C", length = 36) + private String sourceId; + + /** + * ACL target ID. + */ + @Column(name = "ACL_TARGETID_C", length = 36) + private String targetId; + + /** + * Deletion date. + */ + @Column(name = "ACL_DELETEDATE_D") + private Date deleteDate; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public PermType getPerm() { + return perm; + } + + public void setPerm(PermType perm) { + this.perm = perm; + } + + public String getSourceId() { + return sourceId; + } + + public void setSourceId(String sourceId) { + this.sourceId = sourceId; + } + + public String getTargetId() { + return targetId; + } + + public void setTargetId(String targetId) { + this.targetId = targetId; + } + + public Date getDeleteDate() { + return deleteDate; + } + + public void setDeleteDate(Date deleteDate) { + this.deleteDate = deleteDate; + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("id", id) + .add("perm", perm) + .add("sourceId", sourceId) + .add("targetId", targetId) + .toString(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java index 840f85b2..29a3f067 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java @@ -9,7 +9,8 @@ import javax.persistence.Table; import java.util.Date; /** - * File share. + * ACL target used to share a document. + * Can only be used on a single ACL * * @author bgamard */ @@ -26,12 +27,6 @@ public class Share { @Column(name = "SHA_NAME_C", length = 36) private String name; - /** - * Document ID. - */ - @Column(name = "SHA_IDDOCUMENT_C", nullable = false, length = 36) - private String documentId; - /** * Creation date. */ @@ -80,24 +75,6 @@ public class Share { this.name = name; } - /** - * Getter of documentId. - * - * @return the documentId - */ - public String getDocumentId() { - return documentId; - } - - /** - * Setter of documentId. - * - * @param documentId documentId - */ - public void setDocumentId(String documentId) { - this.documentId = documentId; - } - /** * Getter of createDate. * @@ -138,7 +115,6 @@ public class Share { public String toString() { return Objects.toStringHelper(this) .add("id", id) - .add("tagId", documentId) .toString(); } } diff --git a/docs-core/src/main/resources/META-INF/persistence.xml b/docs-core/src/main/resources/META-INF/persistence.xml index eca250e4..51ff2cc5 100644 --- a/docs-core/src/main/resources/META-INF/persistence.xml +++ b/docs-core/src/main/resources/META-INF/persistence.xml @@ -16,5 +16,6 @@ com.sismics.docs.core.model.jpa.Tag com.sismics.docs.core.model.jpa.DocumentTag com.sismics.docs.core.model.jpa.Share + com.sismics.docs.core.model.jpa.Acl \ No newline at end of file diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index f4e9af41..fda5dfd0 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=7 \ No newline at end of file +db.version=8 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-008-0.sql b/docs-core/src/main/resources/db/update/dbupdate-008-0.sql new file mode 100644 index 00000000..639e641b --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-008-0.sql @@ -0,0 +1,4 @@ +create cached table T_ACL ( ACL_ID_C varchar(36) not null, ACL_PERM_C varchar(30) not null, ACL_SOURCEID_C varchar(36) not null, ACL_TARGETID_C varchar(36) not null, ACL_DELETEDATE_D datetime, primary key (ACL_ID_C) ); +drop table T_SHARE; +create cached table T_SHARE ( SHA_ID_C varchar(36) not null, SHA_NAME_C varchar(36), SHA_CREATEDATE_D datetime, SHA_DELETEDATE_D datetime, primary key (SHA_ID_C) ); +update T_CONFIG set CFG_VALUE_C='8' where CFG_ID_C='DB_VERSION'; diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 1bfb8fb0..44ddb414 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=7 \ No newline at end of file +db.version=8 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AclResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AclResource.java new file mode 100644 index 00000000..448ca2b2 --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AclResource.java @@ -0,0 +1,174 @@ +package com.sismics.docs.rest.resource; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +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.AclTargetType; +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.AclDao; +import com.sismics.docs.core.dao.jpa.DocumentDao; +import com.sismics.docs.core.dao.jpa.UserDao; +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.Acl; +import com.sismics.docs.core.model.jpa.Document; +import com.sismics.docs.core.model.jpa.User; +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.ClientException; +import com.sismics.rest.exception.ForbiddenClientException; +import com.sismics.rest.util.ValidationUtil; + +/** + * ACL REST resources. + * + * @author bgamard + */ +@Path("/acl") +public class AclResource extends BaseResource { + /** + * Add an ACL. + * + * @return Response + * @throws JSONException + */ + @PUT + @Produces(MediaType.APPLICATION_JSON) + public Response add(@FormParam("source") String sourceId, + @FormParam("perm") String permStr, + @FormParam("username") String username) throws JSONException { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Validate input + sourceId = ValidationUtil.validateLength(sourceId, "source", 36, 36, false); + PermType perm = PermType.valueOf(ValidationUtil.validateLength(permStr, "perm", 1, 30, false)); + username = ValidationUtil.validateLength(username, "username", 1, 50, false); + + // Validate the target user + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(username); + if (user == null) { + throw new ClientException("UserNotFound", MessageFormat.format("User not found: {0}", username)); + } + + // Check permission on the source by the principal + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(sourceId, PermType.WRITE, principal.getId())) { + throw new ForbiddenClientException(); + } + + // Create the ACL + Acl acl = new Acl(); + acl.setSourceId(sourceId); + acl.setPerm(perm); + acl.setTargetId(user.getId()); + + // Avoid duplicates + if (!aclDao.checkPermission(acl.getSourceId(), acl.getPerm(), acl.getTargetId())) { + aclDao.create(acl); + + // Returns the ACL + JSONObject response = new JSONObject(); + response.put("perm", acl.getPerm().name()); + response.put("id", acl.getTargetId()); + response.put("name", user.getUsername()); + response.put("type", AclTargetType.USER.name()); + return Response.ok().entity(response).build(); + } + + return Response.ok().entity(new JSONObject()).build(); + } + + /** + * Deletes an ACL. + * + * @param id ACL ID + * @return Response + * @throws JSONException + */ + @DELETE + @Path("{sourceId: [a-z0-9\\-]+}/{perm: READ|WRITE}/{targetId: [a-z0-9\\-]+}") + @Produces(MediaType.APPLICATION_JSON) + public Response delete( + @PathParam("sourceId") String sourceId, + @PathParam("perm") String permStr, + @PathParam("targetId") String targetId) throws JSONException { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Validate input + sourceId = ValidationUtil.validateLength(sourceId, "source", 36, 36, false); + PermType perm = PermType.valueOf(ValidationUtil.validateLength(permStr, "perm", 1, 30, false)); + targetId = ValidationUtil.validateLength(targetId, "target", 36, 36, false); + + // Check permission on the source by the principal + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(sourceId, PermType.WRITE, principal.getId())) { + throw new ForbiddenClientException(); + } + + // Cannot delete R/W on a source document if the target is the creator + DocumentDao documentDao = new DocumentDao(); + Document document = documentDao.getById(sourceId); + if (document != null && document.getUserId().equals(targetId)) { + throw new ClientException("AclError", "Cannot delete base ACL on a document"); + } + + // Delete the ACL + aclDao.delete(sourceId, perm, targetId); + + // Always return ok + JSONObject response = new JSONObject(); + response.put("status", "ok"); + return Response.ok().entity(response).build(); + } + + @GET + @Path("target/search") + @Produces(MediaType.APPLICATION_JSON) + public Response targetList(@QueryParam("search") String search) throws JSONException { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Validate input + search = ValidationUtil.validateLength(search, "search", 1, 50, false); + + // Search users + UserDao userDao = new UserDao(); + JSONObject response = new JSONObject(); + List users = new ArrayList<>(); + + PaginatedList paginatedList = PaginatedLists.create(); + SortCriteria sortCriteria = new SortCriteria(1, true); + + userDao.findByCriteria(paginatedList, new UserCriteria().setSearch(search), sortCriteria); + for (UserDto userDto : paginatedList.getResultList()) { + JSONObject user = new JSONObject(); + user.put("username", userDto.getUsername()); + users.add(user); + } + + response.put("users", users); + return Response.ok().entity(response).build(); + } +} 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..5b74078a 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 @@ -6,6 +6,8 @@ import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import javax.persistence.EntityManager; +import javax.persistence.Query; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -20,11 +22,14 @@ import org.apache.log4j.Logger; 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.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.Acl; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.ConfigUtil; import com.sismics.docs.core.util.DirectoryUtil; @@ -34,6 +39,7 @@ 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; +import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.log4j.LogCriteria; import com.sismics.util.log4j.LogEntry; import com.sismics.util.log4j.MemoryAppender; @@ -206,4 +212,58 @@ public class AppResource extends BaseResource { response.put("status", "ok"); return Response.ok().entity(response).build(); } + + /** + * Rebuild ACLs. + * Set Read + Write on documents' creator. + * Loose all sharing. + * + * @return Response + * @throws JSONException + */ + @POST + @Path("batch/rebuild_acls") + @Produces(MediaType.APPLICATION_JSON) + public Response batchRebuildAcls() throws JSONException { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + AclDao aclDao = new AclDao(); + EntityManager em = ThreadLocalContext.get().getEntityManager(); + em.createNativeQuery("truncate table T_ACL").executeUpdate(); + em.createNativeQuery("truncate table T_SHARE").executeUpdate(); + + Query q = em.createNativeQuery("select DOC_ID_C, DOC_IDUSER_C from T_DOCUMENT"); + @SuppressWarnings("unchecked") + List l = q.getResultList(); + for (Object[] o : l) { + String documentId = (String) o[0]; + String userId = (String) o[1]; + + // Create read ACL + Acl acl = new Acl(); + acl.setPerm(PermType.READ); + acl.setSourceId(documentId); + acl.setTargetId(userId); + System.out.println(acl); + aclDao.create(acl); + + // Create write ACL + acl = new Acl(); + acl.setPerm(PermType.WRITE); + acl.setSourceId(documentId); + acl.setTargetId(userId); + System.out.println(acl); + aclDao.create(acl); + } + + int mod = em.createNativeQuery("update T_FILE set FIL_IDUSER_C = (select DOC_IDUSER_C from T_DOCUMENT where DOC_ID_C = FIL_IDDOC_C) where FIL_IDDOC_C is not null").executeUpdate(); + + JSONObject response = new JSONObject(); + response.put("status", "ok"); + response.put("file_id_user_updated", mod); + 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 f8f7774c..57cbc72d 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 @@ -33,11 +33,13 @@ import org.joda.time.format.DateTimeParser; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.AclDao; import com.sismics.docs.core.dao.jpa.DocumentDao; import com.sismics.docs.core.dao.jpa.FileDao; -import com.sismics.docs.core.dao.jpa.ShareDao; import com.sismics.docs.core.dao.jpa.TagDao; import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; +import com.sismics.docs.core.dao.jpa.dto.AclDto; import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.dao.jpa.dto.TagDto; import com.sismics.docs.core.event.DocumentCreatedAsyncEvent; @@ -45,9 +47,9 @@ import com.sismics.docs.core.event.DocumentDeletedAsyncEvent; import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent; import com.sismics.docs.core.event.FileDeletedAsyncEvent; import com.sismics.docs.core.model.context.AppContext; +import com.sismics.docs.core.model.jpa.Acl; import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.File; -import com.sismics.docs.core.model.jpa.Share; import com.sismics.docs.core.model.jpa.Tag; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; @@ -67,7 +69,7 @@ public class DocumentResource extends BaseResource { /** * Returns a document. * - * @param id Document ID + * @param documentId Document ID * @return Response * @throws JSONException */ @@ -75,22 +77,22 @@ public class DocumentResource extends BaseResource { @Path("{id: [a-z0-9\\-]+}") @Produces(MediaType.APPLICATION_JSON) public Response get( - @PathParam("id") String id, + @PathParam("id") String documentId, @QueryParam("share") String shareId) throws JSONException { authenticate(); DocumentDao documentDao = new DocumentDao(); - ShareDao shareDao = new ShareDao(); + AclDao aclDao = new AclDao(); Document documentDb; try { - documentDb = documentDao.getDocument(id); + documentDb = documentDao.getDocument(documentId); // Check document visibility - if (!shareDao.checkVisibility(documentDb, principal.getId(), shareId)) { + if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { throw new ForbiddenClientException(); } } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", id)); + throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); } JSONObject document = new JSONObject(); @@ -99,10 +101,11 @@ public class DocumentResource extends BaseResource { document.put("description", documentDb.getDescription()); document.put("create_date", documentDb.getCreateDate().getTime()); document.put("language", documentDb.getLanguage()); + document.put("creator", documentDb.getUserId()); // Add tags TagDao tagDao = new TagDao(); - List tagDtoList = tagDao.getByDocumentId(id); + List tagDtoList = tagDao.getByDocumentId(documentId); List tags = new ArrayList<>(); for (TagDto tagDto : tagDtoList) { JSONObject tag = new JSONObject(); @@ -113,16 +116,24 @@ public class DocumentResource extends BaseResource { } document.put("tags", tags); - // Add shares - List shareDbList = shareDao.getByDocumentId(id); - List shareList = new ArrayList<>(); - for (Share shareDb : shareDbList) { - JSONObject share = new JSONObject(); - share.put("id", shareDb.getId()); - share.put("name", shareDb.getName()); - shareList.add(share); + // Add ACL + List aclDtoList = aclDao.getBySourceId(documentId); + List aclList = new ArrayList<>(); + boolean writable = false; + for (AclDto aclDto : aclDtoList) { + JSONObject acl = new JSONObject(); + acl.put("perm", aclDto.getPerm().name()); + acl.put("id", aclDto.getTargetId()); + acl.put("name", aclDto.getTargetName()); + acl.put("type", aclDto.getTargetType()); + aclList.add(acl); + + if (aclDto.getTargetId().equals(principal.getId()) && aclDto.getPerm() == PermType.WRITE) { + writable = true; + } } - document.put("shares", shareList); + document.put("acls", aclList); + document.put("writable", writable); return Response.ok().entity(document).build(); } @@ -341,6 +352,21 @@ public class DocumentResource extends BaseResource { } String documentId = documentDao.create(document); + // Create read ACL + AclDao aclDao = new AclDao(); + Acl acl = new Acl(); + acl.setPerm(PermType.READ); + acl.setSourceId(documentId); + acl.setTargetId(principal.getId()); + aclDao.create(acl); + + // Create write ACL + acl = new Acl(); + acl.setPerm(PermType.WRITE); + acl.setSourceId(documentId); + acl.setTargetId(principal.getId()); + aclDao.create(acl); + // Update tags updateTagList(documentId, tagList); @@ -389,7 +415,7 @@ public class DocumentResource extends BaseResource { DocumentDao documentDao = new DocumentDao(); Document document; try { - document = documentDao.getDocument(id, principal.getId()); + document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); } catch (NoResultException e) { throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", id)); } @@ -470,7 +496,7 @@ public class DocumentResource extends BaseResource { Document document; List fileList; try { - document = documentDao.getDocument(id, principal.getId()); + document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); fileList = fileDao.getByDocumentId(principal.getId(), id); } catch (NoResultException e) { throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", id)); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 3b5b7f6f..455b50e2 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -36,9 +36,10 @@ import org.codehaus.jettison.json.JSONObject; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.AclDao; import com.sismics.docs.core.dao.jpa.DocumentDao; import com.sismics.docs.core.dao.jpa.FileDao; -import com.sismics.docs.core.dao.jpa.ShareDao; import com.sismics.docs.core.dao.jpa.UserDao; import com.sismics.docs.core.event.FileCreatedAsyncEvent; import com.sismics.docs.core.event.FileDeletedAsyncEvent; @@ -97,7 +98,7 @@ public class FileResource extends BaseResource { } else { DocumentDao documentDao = new DocumentDao(); try { - document = documentDao.getDocument(documentId, principal.getId()); + document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); } catch (NoResultException e) { throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); } @@ -194,7 +195,7 @@ public class FileResource extends BaseResource { File file; try { file = fileDao.getFile(id, principal.getId()); - document = documentDao.getDocument(documentId, principal.getId()); + document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); } catch (NoResultException e) { throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); } @@ -254,7 +255,7 @@ public class FileResource extends BaseResource { // Get the document DocumentDao documentDao = new DocumentDao(); try { - documentDao.getDocument(documentId, principal.getId()); + documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); } catch (NoResultException e) { throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); } @@ -293,10 +294,8 @@ public class FileResource extends BaseResource { // Check document visibility if (documentId != null) { try { - DocumentDao documentDao = new DocumentDao(); - Document document = documentDao.getDocument(documentId); - ShareDao shareDao = new ShareDao(); - if (!shareDao.checkVisibility(document, principal.getId(), shareId)) { + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { throw new ForbiddenClientException(); } } catch (NoResultException e) { @@ -354,7 +353,7 @@ public class FileResource extends BaseResource { throw new ForbiddenClientException(); } } else { - documentDao.getDocument(file.getDocumentId(), principal.getId()); + documentDao.getDocument(file.getDocumentId(), PermType.WRITE, principal.getId()); } } catch (NoResultException e) { throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", id)); @@ -398,11 +397,8 @@ public class FileResource extends BaseResource { // Get the file FileDao fileDao = new FileDao(); - DocumentDao documentDao = new DocumentDao(); UserDao userDao = new UserDao(); File file; - Document document; - String userId; try { file = fileDao.getFile(fileId); @@ -412,16 +408,10 @@ public class FileResource extends BaseResource { // But not ours throw new ForbiddenClientException(); } - - userId = file.getUserId(); } else { - // It's a file linked to a document - document = documentDao.getDocument(file.getDocumentId()); - userId = document.getUserId(); - - // Check document visibility - ShareDao shareDao = new ShareDao(); - if (!shareDao.checkVisibility(document, principal.getId(), shareId)) { + // Check document accessibility + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(file.getDocumentId(), PermType.READ, shareId == null ? principal.getId() : shareId)) { throw new ForbiddenClientException(); } } @@ -451,7 +441,8 @@ public class FileResource extends BaseResource { // Stream the output and decrypt it if necessary StreamingOutput stream; - User user = userDao.getById(userId); + // A file is always encrypted by the creator of it + User user = userDao.getById(file.getUserId()); try { InputStream fileInputStream = new FileInputStream(storedfile); final InputStream responseInputStream = decrypt ? @@ -500,8 +491,8 @@ public class FileResource extends BaseResource { document = documentDao.getDocument(documentId); // Check document visibility - ShareDao shareDao = new ShareDao(); - if (!shareDao.checkVisibility(document, principal.getId(), shareId)) { + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { throw new ForbiddenClientException(); } } catch (NoResultException e) { @@ -510,9 +501,8 @@ public class FileResource extends BaseResource { // Get files and user associated with this document FileDao fileDao = new FileDao(); - UserDao userDao = new UserDao(); + final UserDao userDao = new UserDao(); final List fileList = fileDao.getByDocumentId(principal.getId(), documentId); - final User user = userDao.getById(document.getUserId()); // Create the ZIP stream StreamingOutput stream = new StreamingOutput() { @@ -526,6 +516,8 @@ public class FileResource extends BaseResource { InputStream fileInputStream = new FileInputStream(storedfile); // Add the decrypted file to the ZIP stream + // Files are encrypted by the creator of them + User user = userDao.getById(file.getUserId()); try (InputStream decryptedStream = EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey())) { ZipEntry zipEntry = new ZipEntry(index + "." + MimeTypeUtil.getFileExtension(file.getMimeType())); zipOutputStream.putNextEntry(zipEntry); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java index 364ccd68..9d0e3d03 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java @@ -2,6 +2,7 @@ package com.sismics.docs.rest.resource; import java.text.MessageFormat; +import java.util.List; import javax.persistence.NoResultException; import javax.ws.rs.DELETE; @@ -16,8 +17,12 @@ import javax.ws.rs.core.Response; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import com.sismics.docs.core.constant.AclTargetType; +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.AclDao; import com.sismics.docs.core.dao.jpa.DocumentDao; import com.sismics.docs.core.dao.jpa.ShareDao; +import com.sismics.docs.core.model.jpa.Acl; import com.sismics.docs.core.model.jpa.Share; import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; @@ -53,7 +58,7 @@ public class ShareResource extends BaseResource { // Get the document DocumentDao documentDao = new DocumentDao(); try { - documentDao.getDocument(documentId, principal.getId()); + documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); } catch (NoResultException e) { throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); } @@ -61,14 +66,23 @@ public class ShareResource extends BaseResource { // Create the share ShareDao shareDao = new ShareDao(); Share share = new Share(); - share.setDocumentId(documentId); share.setName(name); shareDao.create(share); + + // Create the ACL + AclDao aclDao = new AclDao(); + Acl acl = new Acl(); + acl.setSourceId(documentId); + acl.setPerm(PermType.READ); + acl.setTargetId(share.getId()); + aclDao.create(acl); - // Always return ok + // Returns the created ACL JSONObject response = new JSONObject(); - response.put("status", "ok"); - response.put("id", share.getId()); + response.put("perm", acl.getPerm().name()); + response.put("id", acl.getTargetId()); + response.put("name", name); + response.put("type", AclTargetType.SHARE); return Response.ok().entity(response).build(); } @@ -88,23 +102,21 @@ public class ShareResource extends BaseResource { throw new ForbiddenClientException(); } - // Get the share - ShareDao shareDao = new ShareDao(); - DocumentDao documentDao = new DocumentDao(); - Share share = shareDao.getShare(id); - if (share == null) { + // Check that the user can share the linked document + AclDao aclDao = new AclDao(); + List aclList = aclDao.getByTargetId(id); + if (aclList.size() == 0) { throw new ClientException("ShareNotFound", MessageFormat.format("Share not found: {0}", id)); } - // Check that the user is the owner of the linked document - try { - documentDao.getDocument(share.getDocumentId(), principal.getId()); - } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", share.getDocumentId())); + Acl acl = aclList.get(0); + if (!aclDao.checkPermission(acl.getSourceId(), PermType.WRITE, principal.getId())) { + throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", acl.getSourceId())); } // Delete the share - shareDao.delete(share.getId()); + ShareDao shareDao = new ShareDao(); + shareDao.delete(id); // Always return ok JSONObject response = new JSONObject(); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index bcf9a8ab..44939ef4 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -4,6 +4,7 @@ import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao; import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao; import com.sismics.docs.core.dao.jpa.UserDao; +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.AuthenticationToken; import com.sismics.docs.core.model.jpa.User; @@ -19,6 +20,7 @@ import com.sismics.rest.util.ValidationUtil; import com.sismics.security.UserPrincipal; import com.sismics.util.LocaleUtil; import com.sismics.util.filter.TokenBasedSecurityFilter; + import org.apache.commons.lang.StringUtils; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; @@ -517,7 +519,7 @@ public class UserResource extends BaseResource { SortCriteria sortCriteria = new SortCriteria(sortColumn, asc); UserDao userDao = new UserDao(); - userDao.findAll(paginatedList, sortCriteria); + userDao.findByCriteria(paginatedList, new UserCriteria(), sortCriteria); for (UserDto userDto : paginatedList.getResultList()) { JSONObject user = new JSONObject(); user.put("id", userDto.getId()); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js b/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js index a106c32d..4e217334 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js @@ -3,12 +3,22 @@ /** * Document view controller. */ -angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $upload) { +angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $upload, $q) { // Load data from server Restangular.one('document', $stateParams.id).get().then(function(data) { $scope.document = data; }); + // Watch for ACLs change and group them for easy displaying + $scope.$watch('document.acls', function(acls) { + $scope.acls = _.groupBy(acls, function(acl) { + return acl.id; + }); + }); + + // Initialize add ACL + $scope.acl = { perm: 'READ' }; + /** * Configuration for file sorting. */ @@ -17,7 +27,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta forcePlaceholderSize: true, tolerance: 'pointer', handle: '.handle', - stop: function (e, ui) { + stop: function () { // Send new positions to server $scope.$apply(function () { Restangular.one('file').post('reorder', { @@ -103,15 +113,10 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta Restangular.one('share').put({ name: name, id: $stateParams.id - }).then(function (data) { - var share = { - name: name, - id: data.id - }; - - // Display the new share and add it to the local shares - $scope.showShare(share); - $scope.document.shares.push(share); + }).then(function (acl) { + // Display the new share ACL and add it to the local ACLs + $scope.showShare(acl); + $scope.document.acls.push(acl); }) }); }; @@ -135,7 +140,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta if (result == 'unshare') { // Unshare this document and update the local shares Restangular.one('share', share.id).remove().then(function () { - $scope.document.shares = _.reject($scope.document.shares, function(s) { + $scope.document.acls = _.reject($scope.document.acls, function(s) { return share.id == s.id; }); }); @@ -148,6 +153,10 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta * @param files */ $scope.fileDropped = function(files) { + if (!$scope.document.writable) { + return; + } + if (files && files.length) { // Adding files to the UI var newfiles = []; @@ -197,4 +206,42 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta newfile.id = data.id; }); }; + + /** + * Delete an ACL. + * @param acl + */ + $scope.deleteAcl = function(acl) { + Restangular.one('acl/' + $stateParams.id + '/' + acl.perm + '/' + acl.id, null).remove().then(function () { + $scope.document.acls = _.reject($scope.document.acls, function(s) { + return angular.equals(acl, s); + }); + }); + }; + + /** + * Add an ACL. + */ + $scope.addAcl = function() { + $scope.acl.source = $stateParams.id; + Restangular.one('acl').put($scope.acl).then(function(acl) { + $scope.acl = { perm: 'READ' }; + if (_.isUndefined(acl.id)) { + return; + } + $scope.document.acls.push(acl); + $scope.document.acls = angular.copy($scope.document.acls); + }); + }; + + $scope.getTargetAclTypeahead = function($viewValue) { + var deferred = $q.defer(); + Restangular.one('acl/target/search') + .get({ + search: $viewValue + }).then(function(data) { + deferred.resolve(_.pluck(data.users, 'username'), true); + }); + return deferred.promise; + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index 8d19dca3..8fee9ee4 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -104,6 +104,9 @@ {{ errorNumber }} new error{{ errorNumber > 1 ? 's' : '' }} +
  • + {{ userInfo.username }} +
  • Settings diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index 7eb39f0c..3ff3d681 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -1,7 +1,7 @@
    -
    +
    Edit @@ -16,11 +16,11 @@ -

    +

    -

    @@ -30,44 +30,112 @@
  • - -

    -
    -
    -
    -
    - - - -
    -
    -
    + + + + Content + + +

    + +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + +
    +
    +
    -
    - + +
    +

    + {{ file.status }} +

    +
    + +
    -
    -
    -
    -

    - {{ file.status }} +

    + + Drag & drop files here to upload

    -
    - -
    + + + + + Permissions + + + + + + + + + + + + +
    ForPermission
    {{ acl[0].type == 'SHARE' ? 'Shared' : 'User' }} {{ acl[0].name }} + + {{ a.perm }} + + +
    + +
    +

    Add a permission

    + +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + -

    - - Drag & drop files here to upload -

    -
    -
    -
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 39551d4f..1ba0630a 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -179,4 +179,8 @@ input[readonly].share-link { .pointer { cursor: pointer; +} + +.tab-pane { + margin-top: 20px; } \ No newline at end of file diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 1bfb8fb0..44ddb414 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=7 \ No newline at end of file +db.version=8 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java new file mode 100644 index 00000000..38618790 --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java @@ -0,0 +1,177 @@ +package com.sismics.docs.rest; + +import java.util.Date; + +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; + +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; + +/** + * Test the ACL resource. + * + * @author bgamard + */ +public class TestAclResource extends BaseJerseyTest { + /** + * Test the ACL resource. + * + * @throws JSONException + */ + @Test + public void testAclResource() throws JSONException { + // Login acl1 + clientUtil.createUser("acl1"); + String acl1Token = clientUtil.login("acl1"); + + // Login acl2 + clientUtil.createUser("acl2"); + String acl2Token = clientUtil.login("acl2"); + + // Create a document + WebResource documentResource = resource().path("/document"); + documentResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + MultivaluedMapImpl postParams = new MultivaluedMapImpl(); + postParams.add("title", "My super title document 1"); + postParams.add("language", "eng"); + postParams.add("create_date", new Date().getTime()); + ClientResponse response = documentResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + JSONObject json = response.getEntity(JSONObject.class); + String document1Id = json.optString("id"); + + // Get the document as acl1 + documentResource = resource().path("/document/" + document1Id); + documentResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + Assert.assertEquals(document1Id, json.getString("id")); + JSONArray acls = json.getJSONArray("acls"); + Assert.assertEquals(2, acls.length()); + + // Get the document as acl2 + documentResource = resource().path("/document/" + document1Id); + documentResource.addFilter(new CookieAuthenticationFilter(acl2Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus())); + + // Add an ACL READ for acl2 with acl1 + WebResource aclResource = resource().path("/acl"); + aclResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("source", document1Id); + postParams.add("perm", "READ"); + postParams.add("username", "acl2"); + response = aclResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + String acl2Id = json.getString("id"); + + // Add an ACL WRITE for acl2 with acl1 + aclResource = resource().path("/acl"); + aclResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("source", document1Id); + postParams.add("perm", "WRITE"); + postParams.add("username", "acl2"); + response = aclResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + + // Add an ACL WRITE for acl2 with acl1 (again) + aclResource = resource().path("/acl"); + aclResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("source", document1Id); + postParams.add("perm", "WRITE"); + postParams.add("username", "acl2"); + response = aclResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + + // Get the document as acl1 + documentResource = resource().path("/document/" + document1Id); + documentResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + Assert.assertEquals(document1Id, json.getString("id")); + acls = json.getJSONArray("acls"); + Assert.assertEquals(4, acls.length()); + + // Get the document as acl2 + documentResource = resource().path("/document/" + document1Id); + documentResource.addFilter(new CookieAuthenticationFilter(acl2Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + Assert.assertEquals(document1Id, json.getString("id")); + acls = json.getJSONArray("acls"); + Assert.assertEquals(4, acls.length()); + + // Delete the ACL WRITE for acl2 with acl2 + aclResource = resource().path("/acl/" + document1Id + "/WRITE/" + acl2Id); + aclResource.addFilter(new CookieAuthenticationFilter(acl2Token)); + response = aclResource.delete(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + + // Delete the ACL READ for acl2 with acl2 + aclResource = resource().path("/acl/" + document1Id + "/READ/" + acl2Id); + aclResource.addFilter(new CookieAuthenticationFilter(acl2Token)); + response = aclResource.delete(ClientResponse.class); + Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus())); + + // Delete the ACL READ for acl2 with acl1 + aclResource = resource().path("/acl/" + document1Id + "/READ/" + acl2Id); + aclResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + response = aclResource.delete(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + + // Get the document as acl1 + documentResource = resource().path("/document/" + document1Id); + documentResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + Assert.assertEquals(document1Id, json.getString("id")); + acls = json.getJSONArray("acls"); + Assert.assertEquals(2, acls.length()); + String acl1Id = acls.getJSONObject(0).getString("id"); + + // Get the document as acl2 + documentResource = resource().path("/document/" + document1Id); + documentResource.addFilter(new CookieAuthenticationFilter(acl2Token)); + response = documentResource.get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus())); + + // Delete the ACL READ for acl1 with acl1 + aclResource = resource().path("/acl/" + document1Id + "/READ/" + acl1Id); + aclResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + response = aclResource.delete(ClientResponse.class); + Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus())); + + // Delete the ACL WRITE for acl1 with acl1 + aclResource = resource().path("/acl/" + document1Id + "/WRITE/" + acl1Id); + aclResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + response = aclResource.delete(ClientResponse.class); + Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus())); + + // Search target list + aclResource = resource().path("/acl/target/search"); + aclResource.addFilter(new CookieAuthenticationFilter(acl1Token)); + response = aclResource.queryParam("search", "acl").get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + JSONArray users = json.getJSONArray("users"); + Assert.assertEquals(2, users.length()); + } +} \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index 78ba8535..24168ec9 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -43,6 +43,10 @@ public class TestDocumentResource extends BaseJerseyTest { clientUtil.createUser("document1"); String document1Token = clientUtil.login("document1"); + // Login document3 + clientUtil.createUser("document3"); + String document3Token = clientUtil.login("document3"); + // Create a tag WebResource tagResource = resource().path("/tag"); tagResource.addFilter(new CookieAuthenticationFilter(document1Token)); @@ -115,6 +119,61 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals("SuperTag", tags.getJSONObject(0).getString("name")); Assert.assertEquals("#ffff00", tags.getJSONObject(0).getString("color")); + // List all documents from document3 + documentResource = resource().path("/document/list"); + documentResource.addFilter(new CookieAuthenticationFilter(document3Token)); + getParams = new MultivaluedMapImpl(); + getParams.putSingle("sort_column", 3); + getParams.putSingle("asc", false); + response = documentResource.queryParams(getParams).get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + documents = json.getJSONArray("documents"); + Assert.assertTrue(documents.length() == 0); + + // Create a document with document3 + documentResource = resource().path("/document"); + documentResource.addFilter(new CookieAuthenticationFilter(document3Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("title", "My super title document 1"); + postParams.add("description", "My super description for document 1"); + postParams.add("language", "eng"); + 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 document3Id = json.optString("id"); + Assert.assertNotNull(document3Id); + + // Add a file + fileResource = resource().path("/file"); + fileResource.addFilter(new CookieAuthenticationFilter(document1Token)); + form = new FormDataMultiPart(); + file = this.getClass().getResourceAsStream("/file/Einstein-Roosevelt-letter.png"); + fdp = new FormDataBodyPart("file", + new BufferedInputStream(file), + MediaType.APPLICATION_OCTET_STREAM_TYPE); + form.bodyPart(fdp); + form.field("id", document1Id); + response = fileResource.type(MediaType.MULTIPART_FORM_DATA).put(ClientResponse.class, form); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + String file3Id = json.getString("id"); + Assert.assertNotNull(file3Id); + + // List all documents from document3 + documentResource = resource().path("/document/list"); + documentResource.addFilter(new CookieAuthenticationFilter(document3Token)); + getParams = new MultivaluedMapImpl(); + getParams.putSingle("sort_column", 3); + getParams.putSingle("asc", false); + response = documentResource.queryParams(getParams).get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + documents = json.getJSONArray("documents"); + Assert.assertTrue(documents.length() == 1); + // Search documents Assert.assertEquals(1, searchDocuments("full:uranium full:einstein", document1Token)); Assert.assertEquals(1, searchDocuments("full:title", document1Token)); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestLocaleResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestLocaleResource.java index 6b2923d9..cbc24a5b 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestLocaleResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestLocaleResource.java @@ -24,7 +24,6 @@ public class TestLocaleResource extends BaseJerseyTest { public void testLocaleResource() throws JSONException { WebResource localeResource = resource().path("/locale"); ClientResponse response = localeResource.get(ClientResponse.class); - response = localeResource.get(ClientResponse.class); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); JSONObject json = response.getEntity(JSONObject.class); JSONArray locale = json.getJSONArray("locales"); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java index e69ca0f6..2333d4e1 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java @@ -83,9 +83,7 @@ public class TestShareResource extends BaseJerseyTest { json = response.getEntity(JSONObject.class); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); Assert.assertEquals(document1Id, json.getString("id")); - Assert.assertEquals(1, json.getJSONArray("shares").length()); - Assert.assertEquals(share1Id, json.getJSONArray("shares").getJSONObject(0).getString("id")); - Assert.assertEquals("4 All", json.getJSONArray("shares").getJSONObject(0).getString("name")); + Assert.assertEquals(3, json.getJSONArray("acls").length()); // 2 for the creator, 1 for the share // Get all files from this document anonymously fileResource = resource().path("/file/list");