#13: ACL system

This commit is contained in:
jendib 2015-05-09 14:44:19 +02:00
parent 6ff639baac
commit fc1bb22d8d
30 changed files with 1197 additions and 231 deletions

View File

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

View File

@ -0,0 +1,18 @@
package com.sismics.docs.core.constant;
/**
* Permissions.
*
* @author bgamard
*/
public enum PermType {
/**
* Read document.
*/
READ,
/**
* Write document.
*/
WRITE
}

View File

@ -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<Acl> 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<AclDto> 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<Object[]> l = q.getResultList();
// Assemble results
List<AclDto> aclDtoList = new ArrayList<AclDto>();
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();
}
}

View File

@ -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<String, Object> parameterMap = new HashMap<String, Object>();
List<String> criteriaList = new ArrayList<String>();
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<String> documentIdList = luceneDao.search(criteria.getUserId(), criteria.getSearch(), criteria.getFullSearch());
Set<String> 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);
}

View File

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

View File

@ -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<UserDto> paginatedList, SortCriteria sortCriteria) {
public void findByCriteria(PaginatedList<UserDto> paginatedList, UserCriteria criteria, SortCriteria sortCriteria) {
Map<String, Object> parameterMap = new HashMap<String, Object>();
List<String> criteriaList = new ArrayList<String>();
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<String> criteriaList = new ArrayList<String>();
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()) {

View File

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

View File

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

View File

@ -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<String> search(String userId, String searchQuery, String fullSearchQuery) throws Exception {
public Set<String> 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<Term> terms = new ArrayList<Term>();
if (userId != null) {
terms.add(new Term("user_id", userId));
}
TermsFilter userFilter = new TermsFilter(terms);
// Search
DirectoryReader directoryReader = AppContext.getInstance().getIndexingService().getDirectoryReader();
Set<String> documentIdList = new HashSet<String>();
@ -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) {

View File

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

View File

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

View File

@ -16,5 +16,6 @@
<class>com.sismics.docs.core.model.jpa.Tag</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.Acl</class>
</persistence-unit>
</persistence>

View File

@ -1 +1 @@
db.version=7
db.version=8

View File

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

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=7
db.version=8

View File

@ -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<JSONObject> users = new ArrayList<>();
PaginatedList<UserDto> 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();
}
}

View File

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

View File

@ -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<TagDto> tagDtoList = tagDao.getByDocumentId(id);
List<TagDto> tagDtoList = tagDao.getByDocumentId(documentId);
List<JSONObject> 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<Share> shareDbList = shareDao.getByDocumentId(id);
List<JSONObject> 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<AclDto> aclDtoList = aclDao.getBySourceId(documentId);
List<JSONObject> 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<File> 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));

View File

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

View File

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

View File

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

View File

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

View File

@ -104,6 +104,9 @@
<span class="glyphicon glyphicon-warning-sign"></span> {{ errorNumber }} new error{{ errorNumber > 1 ? 's' : '' }}
</a>
</li>
<li>
<a href="#/settings/account" title="Logged in as {{ userInfo.username }}">{{ userInfo.username }}</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/settings.*">
<a href="#/settings/account">
<span class="glyphicon glyphicon-cog"></span> Settings

View File

@ -1,7 +1,7 @@
<img src="img/loader.gif" ng-show="!document" />
<div ng-show="document">
<div class="text-right">
<div class="text-right" ng-show="document.writable">
<div class="btn-group">
<button class="btn btn-danger" ng-click="deleteDocument(document)"><span class="glyphicon glyphicon-trash"></span> Delete</button>
<a href="#/document/edit/{{ document.id }}" class="btn btn-primary"><span class="glyphicon glyphicon-pencil"></span> Edit</a>
@ -16,11 +16,11 @@
<span class="glyphicon glyphicon-download-alt"></span>
</a>
</h1>
<p>
<p ng-show="document.writable">
<button class="btn btn-sm btn-info" ng-click="share()">
<span class="glyphicon glyphicon-share"></span> Share
</button>
<button class="btn btn-default btn-sm" ng-repeat="share in document.shares" ng-click="showShare(share)">
<button class="btn btn-default btn-sm" ng-repeat="share in document.acls | filter: { 'type': 'SHARE' }" ng-click="showShare(share)">
<span class="glyphicon glyphicon-ok"></span> {{ share.name ? share.name : 'shared' }}
</button>
</p>
@ -30,44 +30,112 @@
</li>
</ul>
</div>
<p ng-bind-html="document.description | newline"></p>
<div ng-file-drop drag-over-class="bg-success" ng-multiple="true" allow-dir="false" ng-model="dropFiles"
accept="image/*,application/pdf,application/zip" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
<div class="row upload-zone" ui-sortable="fileSortableOptions" ng-model="files">
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files">
<div class="thumbnail" ng-if="file.id">
<a ng-click="openFile(file)">
<img class="thumbnail-file" ng-src="../api/file/{{ file.id }}/data?size=thumb" tooltip="{{ file.mimetype }}" tooltip-placement="top" />
</a>
<div class="caption">
<div class="pull-left">
<div class="btn btn-default handle"><span class="glyphicon glyphicon-resize-horizontal"></span></div>
<tabset>
<tab>
<tab-heading class="pointer">
<span class="glyphicon glyphicon-file"></span> Content
</tab-heading>
<p ng-bind-html="document.description | newline"></p>
<div ng-file-drop drag-over-class="bg-success" ng-multiple="true" allow-dir="false" ng-model="dropFiles"
accept="image/*,application/pdf,application/zip" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
<div class="row upload-zone" ui-sortable="fileSortableOptions" ng-model="files">
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files">
<div class="thumbnail" ng-if="file.id">
<a ng-click="openFile(file)">
<img class="thumbnail-file" ng-src="../api/file/{{ file.id }}/data?size=thumb" tooltip="{{ file.mimetype }}" tooltip-placement="top" />
</a>
<div class="caption" ng-show="document.writable">
<div class="pull-left">
<div class="btn btn-default handle"><span class="glyphicon glyphicon-resize-horizontal"></span></div>
</div>
<div class="pull-right">
<button class="btn btn-danger" ng-click="deleteFile(file)"><span class="glyphicon glyphicon-trash"></span></button>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="pull-right">
<button class="btn btn-danger" ng-click="deleteFile(file)"><span class="glyphicon glyphicon-trash"></span></button>
<div class="thumbnail" ng-if="!file.id">
<p class="text-center lead">
{{ file.status }}
</p>
<div class="caption">
<progressbar value="file.progress" class="progress-info active"></progressbar>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="thumbnail" ng-if="!file.id">
<p class="text-center lead">
{{ file.status }}
<p class="text-center well-lg" ng-if="files.length == 0">
<span class="glyphicon glyphicon-move"></span>
Drag &amp; drop files here to upload
</p>
<div class="caption">
<progressbar value="file.progress" class="progress-info active"></progressbar>
</div>
</div>
</div>
</tab>
<tab>
<tab-heading class="pointer">
<span class="glyphicon glyphicon-user"></span> Permissions
</tab-heading>
<table class="table">
<tr>
<th style="width: 40%">For</th>
<th style="width: 40%">Permission</th>
</tr>
<tr ng-repeat="(id, acl) in acls">
<td><em>{{ acl[0].type == 'SHARE' ? 'Shared' : 'User' }}</em> {{ acl[0].name }}</td>
<td>
<span class="label label-default" style="margin-right: 6px;" ng-repeat="a in acl | orderBy: 'perm'">
{{ a.perm }}
<span ng-show="document.creator != a.id && document.writable"
class="glyphicon glyphicon-remove pointer"
ng-click="deleteAcl(a)"></span>
</span>
</td>
</tr>
</table>
<div ng-show="document.writable">
<h4>Add a permission</h4>
<form name="aclForm" class="form-horizontal">
<div class="form-group">
<label class=" col-sm-2 control-label" for="inputTarget">User</label>
<div class="col-sm-3">
<input required ng-maxlength="50" class="form-control" type="text" id="inputTarget"
placeholder="Type a username" name="username" ng-model="acl.username" autocomplete="off"
typeahead="username for username in getTargetAclTypeahead($viewValue) | filter: $viewValue"
typeahead-wait-ms="200" />
</div>
</div>
<div class="form-group">
<label class=" col-sm-2 control-label" for="inputPermission">Permission</label>
<div class="col-sm-3">
<select class="form-control" ng-model="acl.perm" id="inputPermission">
<option value="READ">Read</option>
<option value="WRITE">Write</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-disabled="!aclForm.$valid" ng-click="addAcl()">
<span class="glyphicon glyphicon-plus"></span>
Add
</button>
</div>
</div>
</form>
</div>
</tab>
</tabset>
<p class="text-center well-lg" ng-if="files.length == 0">
<span class="glyphicon glyphicon-move"></span>
Drag &amp; drop files here to upload
</p>
</div>
</div>
<div ui-view="file"></div>
</div>

View File

@ -179,4 +179,8 @@ input[readonly].share-link {
.pointer {
cursor: pointer;
}
.tab-pane {
margin-top: 20px;
}

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=7
db.version=8

View File

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

View File

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

View File

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

View File

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