diff --git a/README.md b/README.md index 887d6878..290e66c8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Features - 256-bit AES encryption - Tag system with relations - Multi-users ACL system +- Hierarchical groups - Audit log - Comments - Storage quota per user diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java index 166b0185..9e65c3c7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AclDao.java @@ -68,10 +68,11 @@ public class AclDao { @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"); + 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, g.GRP_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(" left join T_GROUP g on g.GRP_ID_C = a.ACL_TARGETID_C "); sb.append(" where a.ACL_DELETEDATE_D is null and a.ACL_SOURCEID_C = :sourceId "); // Perform the query @@ -89,9 +90,19 @@ public class AclDao { 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()); + String groupName = (String) o[i++]; + if (userName != null) { + aclDto.setTargetName(userName); + aclDto.setTargetType(AclTargetType.USER.name()); + } + if (shareName != null) { + aclDto.setTargetName(shareName); + aclDto.setTargetType(AclTargetType.SHARE.name()); + } + if (groupName != null) { + aclDto.setTargetName(groupName); + aclDto.setTargetType(AclTargetType.GROUP.name()); + } aclDtoList.add(aclDto); } return aclDtoList; 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 012f5926..aab9bdbe 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 @@ -92,13 +92,14 @@ public class DocumentDao { */ public DocumentDto getDocument(String id, PermType perm, List targetIdList) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - StringBuilder sb = new StringBuilder("select d.DOC_ID_C, d.DOC_TITLE_C, d.DOC_DESCRIPTION_C, d.DOC_SUBJECT_C, d.DOC_IDENTIFIER_C, d.DOC_PUBLISHER_C, d.DOC_FORMAT_C, d.DOC_SOURCE_C, d.DOC_TYPE_C, d.DOC_COVERAGE_C, d.DOC_RIGHTS_C, d.DOC_CREATEDATE_D, d.DOC_LANGUAGE_C, "); + StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C, d.DOC_TITLE_C, d.DOC_DESCRIPTION_C, d.DOC_SUBJECT_C, d.DOC_IDENTIFIER_C, d.DOC_PUBLISHER_C, d.DOC_FORMAT_C, d.DOC_SOURCE_C, d.DOC_TYPE_C, d.DOC_COVERAGE_C, d.DOC_RIGHTS_C, d.DOC_CREATEDATE_D, d.DOC_LANGUAGE_C, "); 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), "); 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), "); sb.append(" u.USE_USERNAME_C "); - sb.append(" from T_DOCUMENT d, T_USER u "); - sb.append(" join T_ACL a on a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_TARGETID_C in (:targetIdList) and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null "); - sb.append(" where d.DOC_IDUSER_C = u.USE_ID_C and d.DOC_ID_C = :id and d.DOC_DELETEDATE_D is null "); + sb.append(" from T_DOCUMENT d "); + sb.append(" join T_USER u on d.DOC_IDUSER_C = u.USE_ID_C "); + sb.append(" left join T_ACL a on a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_TARGETID_C in (:targetIdList) and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null "); + sb.append(" where d.DOC_ID_C = :id and a.ACL_ID_C is not null and d.DOC_DELETEDATE_D is null "); Query q = em.createNativeQuery(sb.toString()); q.setParameter("id", id); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java index 337a1321..c22af1c1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/GroupDao.java @@ -1,16 +1,28 @@ package com.sismics.docs.core.dao.jpa; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.Query; +import com.google.common.base.Joiner; import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria; +import com.sismics.docs.core.dao.jpa.dto.GroupDto; import com.sismics.docs.core.model.jpa.Group; import com.sismics.docs.core.model.jpa.UserGroup; import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.docs.core.util.jpa.QueryParam; +import com.sismics.docs.core.util.jpa.QueryUtil; +import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; /** @@ -25,7 +37,7 @@ public class GroupDao { * @param name Name * @return Tag */ - public Group getByName(String name) { + public Group getActiveByName(String name) { EntityManager em = ThreadLocalContext.get().getEntityManager(); Query q = em.createQuery("select g from Group g where g.name = :name and g.deleteDate is null"); q.setParameter("name", name); @@ -81,6 +93,11 @@ public class GroupDao { q.setParameter("dateNow", dateNow); q.setParameter("groupId", groupId); q.executeUpdate(); + + q = em.createQuery("update Acl a set a.deleteDate = :dateNow where a.targetId = :groupId and a.deleteDate is null"); + q.setParameter("groupId", groupDb.getId()); + q.setParameter("dateNow", dateNow); + q.executeUpdate(); // Create audit log AuditLogUtil.create(groupDb, AuditLogType.DELETE, userId); @@ -108,18 +125,107 @@ public class GroupDao { * Remove an user from a group. * * @param groupId Group ID + * @param userId User ID */ - public void removeMember(String userGroupId) { + public void removeMember(String groupId, String userId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); // Get the user group - Query q = em.createQuery("select ug from UserGroup ug where ug.id = :id and ug.deleteDate is null"); - q.setParameter("id", userGroupId); + Query q = em.createQuery("select ug from UserGroup ug where ug.groupId = :groupId and ug.userId = :userId and ug.deleteDate is null"); + q.setParameter("groupId", groupId); + q.setParameter("userId", userId); UserGroup userGroupDb = (UserGroup) q.getSingleResult(); // Delete the user group Date dateNow = new Date(); userGroupDb.setDeleteDate(dateNow); } + + /** + * Returns the list of all groups. + * + * @param criteria Search criteria + * @param sortCriteria Sort criteria + * @return List of groups + */ + public List findByCriteria(GroupCriteria criteria, SortCriteria sortCriteria) { + Map parameterMap = new HashMap(); + List criteriaList = new ArrayList(); + + StringBuilder sb = new StringBuilder("select g.GRP_ID_C as c0, g.GRP_NAME_C as c1, g.GRP_IDPARENT_C as c2, ug.UGP_ID_C "); + sb.append(" from T_GROUP g "); + + // Add search criterias + if (criteria.getSearch() != null) { + criteriaList.add("lower(g.GRP_NAME_C) like lower(:search)"); + parameterMap.put("search", "%" + criteria.getSearch() + "%"); + } + if (criteria.getUserId() != null) { + // Left join and post-filtering for recursive groups + sb.append((criteria.isRecursive() ? " left " : "") + + " join T_USER_GROUP ug on ug.UGP_IDGROUP_C = g.GRP_ID_C and ug.UGP_IDUSER_C = :userId and ug.UGP_DELETEDATE_D is null "); + parameterMap.put("userId", criteria.getUserId()); + } + + criteriaList.add("g.GRP_DELETEDATE_D is null"); + + if (!criteriaList.isEmpty()) { + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); + } + + // Perform the search + QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); + @SuppressWarnings("unchecked") + List l = QueryUtil.getNativeQuery(queryParam).getResultList(); + + // Assemble results + List groupDtoList = new ArrayList<>(); + List userGroupDtoList = new ArrayList<>(); + for (Object[] o : l) { + int i = 0; + GroupDto groupDto = new GroupDto() + .setId((String) o[i++]) + .setName((String) o[i++]) + .setParentId((String) o[i++]); + groupDtoList.add(groupDto); + if (o[i++] != null) { + userGroupDtoList.add(groupDto); + } + } + + // Post-query filtering for recursive groups + if (criteria.getUserId() != null && criteria.isRecursive()) { + Set filteredGroupDtoSet = new HashSet<>(); + for (GroupDto userGroupDto : userGroupDtoList) { + filteredGroupDtoSet.add(userGroupDto); // Direct group + findGroupParentHierarchy(filteredGroupDtoSet, groupDtoList, userGroupDto, 0); // Indirect groups + } + groupDtoList = new ArrayList<>(filteredGroupDtoSet); + } + + return groupDtoList; + } + + /** + * Recursively search group's parents. + * + * @param parentGroupDtoSet Resulting parents + * @param groupDtoList All groups + * @param userGroupDto Reference group to search from + * @param depth Depth + */ + private void findGroupParentHierarchy(Set parentGroupDtoSet, List groupDtoList, GroupDto userGroupDto, int depth) { + if (userGroupDto.getParentId() == null || depth == 10) { // Max depth 10 to avoid infinite loop + return; + } + + for (GroupDto groupDto : groupDtoList) { + if (groupDto.getId().equals(userGroupDto.getParentId())) { + parentGroupDtoSet.add(groupDto); // Add parent + findGroupParentHierarchy(parentGroupDtoSet, groupDtoList, groupDto, depth + 1); // Find parent's parents + } + } + } } 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 ff005b5d..c502621e 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 @@ -20,9 +20,8 @@ import com.sismics.docs.core.dao.jpa.criteria.UserCriteria; import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.AuditLogUtil; -import com.sismics.docs.core.util.jpa.PaginatedList; -import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.QueryParam; +import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; @@ -265,10 +264,11 @@ public class UserDao { /** * Returns the list of all users. * - * @param paginatedList List of users (updated by side effects) + * @param criteria Search criteria * @param sortCriteria Sort criteria + * @return List of users */ - public void findByCriteria(PaginatedList paginatedList, UserCriteria criteria, SortCriteria sortCriteria) { + public List findByCriteria(UserCriteria criteria, SortCriteria sortCriteria) { Map parameterMap = new HashMap(); List criteriaList = new ArrayList(); @@ -289,8 +289,9 @@ public class UserDao { } // Perform the search - QueryParam queryParam = new QueryParam(sb.toString(), parameterMap); - List l = PaginatedLists.executePaginatedQuery(paginatedList, queryParam, sortCriteria); + QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); + @SuppressWarnings("unchecked") + List l = QueryUtil.getNativeQuery(queryParam).getResultList(); // Assemble results List userDtoList = new ArrayList(); @@ -305,6 +306,6 @@ public class UserDao { userDto.setStorageQuota(((Number) o[i++]).longValue()); userDtoList.add(userDto); } - paginatedList.setResultList(userDtoList); + return userDtoList; } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/GroupCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/GroupCriteria.java new file mode 100644 index 00000000..a85a8876 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/criteria/GroupCriteria.java @@ -0,0 +1,50 @@ +package com.sismics.docs.core.dao.jpa.criteria; + +/** + * Group criteria. + * + * @author bgamard + */ +public class GroupCriteria { + /** + * Search query. + */ + private String search; + + /** + * User ID. + */ + private String userId; + + /** + * Retrieve user groups recursively. + */ + private boolean recursive = false; + + public String getSearch() { + return search; + } + + public GroupCriteria setSearch(String search) { + this.search = search; + return this; + } + + public String getUserId() { + return userId; + } + + public GroupCriteria setUserId(String userId) { + this.userId = userId; + return this; + } + + public boolean isRecursive() { + return recursive; + } + + public GroupCriteria setRecursive(boolean recursive) { + this.recursive = recursive; + return this; + } +} 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 index 99cf7394..74d84ed6 100644 --- 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 @@ -1,7 +1,5 @@ package com.sismics.docs.core.dao.jpa.criteria; - - /** * User criteria. * diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/GroupDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/GroupDto.java new file mode 100644 index 00000000..4c088755 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/GroupDto.java @@ -0,0 +1,60 @@ +package com.sismics.docs.core.dao.jpa.dto; + +/** + * Group DTO. + * + * @author bgamard + */ +public class GroupDto { + /** + * Group ID. + */ + private String id; + + /** + * Name. + */ + private String name; + + /** + * Parent ID. + */ + private String parentId; + + public String getId() { + return id; + } + + public GroupDto setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public GroupDto setName(String name) { + this.name = name; + return this; + } + + public String getParentId() { + return parentId; + } + + public GroupDto setParentId(String parentId) { + this.parentId = parentId; + return this; + } + + @Override + public boolean equals(Object obj) { + return id.equals(((GroupDto) obj).getId()); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java index a8331909..b0cc166b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/UserDto.java @@ -1,6 +1,5 @@ package com.sismics.docs.core.dao.jpa.dto; - /** * User DTO. * diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java index c1c6affe..d415a3a0 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java @@ -105,13 +105,7 @@ public class PaginatedLists { * @return List of results */ public static List executePaginatedQuery(PaginatedList paginatedList, QueryParam queryParam, SortCriteria sortCriteria) { - StringBuilder sb = new StringBuilder(queryParam.getQueryString()); - sb.append(" order by c"); - sb.append(sortCriteria.getColumn()); - sb.append(sortCriteria.isAsc() ? " asc" : " desc"); - - QueryParam sortedQueryParam = new QueryParam(sb.toString(), queryParam.getParameterMap()); - + QueryParam sortedQueryParam = QueryUtil.getSortedQueryParam(queryParam, sortCriteria); executeCountQuery(paginatedList, sortedQueryParam); return executeResultQuery(paginatedList, sortedQueryParam); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java index fa9090f9..3575636e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java @@ -1,10 +1,11 @@ package com.sismics.docs.core.util.jpa; -import com.sismics.util.context.ThreadLocalContext; +import java.util.Map.Entry; import javax.persistence.EntityManager; import javax.persistence.Query; -import java.util.Map.Entry; + +import com.sismics.util.context.ThreadLocalContext; /** * Query utilities. @@ -27,4 +28,22 @@ public class QueryUtil { } return query; } + + /** + * Returns sorted query parameters. + * + * @param queryParam Query parameters + * @param sortCriteria Sort criteria + * @return Sorted query parameters + */ + public static QueryParam getSortedQueryParam(QueryParam queryParam, SortCriteria sortCriteria) { + StringBuilder sb = new StringBuilder(queryParam.getQueryString()); + if (sortCriteria != null) { + sb.append(" order by c"); + sb.append(sortCriteria.getColumn()); + sb.append(sortCriteria.isAsc() ? " asc" : " desc"); + } + + return new QueryParam(sb.toString(), queryParam.getParameterMap()); + } } diff --git a/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java b/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java index 530935b5..5becb3ab 100644 --- a/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java +++ b/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java @@ -1,10 +1,10 @@ package com.sismics.security; -import java.util.List; +import java.util.Set; import org.joda.time.DateTimeZone; -import com.google.common.collect.Lists; +import jersey.repackaged.com.google.common.collect.Sets; /** * Anonymous principal. @@ -56,7 +56,7 @@ public class AnonymousPrincipal implements IPrincipal { } @Override - public List getGroupIdList() { - return Lists.newArrayList(); + public Set getGroupIdSet() { + return Sets.newHashSet(); } } diff --git a/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java b/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java index 0e965a0f..3129f363 100644 --- a/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java +++ b/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java @@ -1,7 +1,7 @@ package com.sismics.security; import java.security.Principal; -import java.util.List; +import java.util.Set; import org.joda.time.DateTimeZone; @@ -31,7 +31,7 @@ public interface IPrincipal extends Principal { * * @return List of group ID */ - public List getGroupIdList(); + public Set getGroupIdSet(); /** * Returns the timezone of the principal. diff --git a/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java b/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java index e12c987f..bfef8fe3 100644 --- a/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java +++ b/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java @@ -1,12 +1,9 @@ package com.sismics.security; -import java.util.List; import java.util.Set; import org.joda.time.DateTimeZone; -import jersey.repackaged.com.google.common.collect.Lists; - /** * Authenticated users principal. * @@ -38,6 +35,11 @@ public class UserPrincipal implements IPrincipal { */ private Set baseFunctionSet; + /** + * User groups. + */ + private Set groupIdSet; + /** * Constructor of UserPrincipal. * @@ -99,8 +101,11 @@ public class UserPrincipal implements IPrincipal { } @Override - public List getGroupIdList() { - // TODO Real groups - return Lists.newArrayList("members"); + public Set getGroupIdSet() { + return groupIdSet; + } + + public void setGroupIdSet(Set groupIdSet) { + this.groupIdSet = groupIdSet; } } diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java index 2e65cc9a..5be61d6c 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java @@ -3,6 +3,7 @@ package com.sismics.util.filter; import java.io.IOException; import java.text.MessageFormat; import java.util.Date; +import java.util.List; import java.util.Set; import javax.servlet.Filter; @@ -20,14 +21,18 @@ import org.slf4j.LoggerFactory; import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao; +import com.sismics.docs.core.dao.jpa.GroupDao; import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao; import com.sismics.docs.core.dao.jpa.UserDao; +import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria; +import com.sismics.docs.core.dao.jpa.dto.GroupDto; import com.sismics.docs.core.model.jpa.AuthenticationToken; import com.sismics.docs.core.model.jpa.User; -import com.sismics.docs.core.util.TransactionUtil; import com.sismics.security.AnonymousPrincipal; import com.sismics.security.UserPrincipal; +import jersey.repackaged.com.google.common.collect.Sets; + /** * This filter is used to authenticate the user having an active session via an authentication token stored in database. * The filter extracts the authentication token stored in a cookie. @@ -113,10 +118,6 @@ public class TokenBasedSecurityFilter implements Filter { User user = userDao.getById(authenticationToken.getUserId()); if (user != null && user.getDeleteDate() == null) { injectAuthenticatedUser(request, user); - - // Update the last connection date - authenticationTokenDao.updateLastConnectionDate(authenticationToken.getId()); - TransactionUtil.commit(); } else { injectAnonymousUser(request); } @@ -158,6 +159,17 @@ public class TokenBasedSecurityFilter implements Filter { Set baseFunctionSet = userBaseFuction.findByRoleId(user.getRoleId()); userPrincipal.setBaseFunctionSet(baseFunctionSet); + // Add groups + GroupDao groupDao = new GroupDao(); + List groupDtoList = groupDao.findByCriteria(new GroupCriteria() + .setUserId(user.getId()) + .setRecursive(true), null); + Set groupIdSet = Sets.newHashSet(); + for (GroupDto groupDto : groupDtoList) { + groupIdSet.add(groupDto.getId()); + } + userPrincipal.setGroupIdSet(groupIdSet); + // Add email userPrincipal.setEmail(user.getEmail()); diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java index 3f16562c..4c476265 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java @@ -31,22 +31,59 @@ public class ClientUtil { * * @param username Username */ - public void createUser(String username) { + public void createUser(String username, String... groupNameList) { // Login admin to create the user - String adminAuthenticationToken = login("admin", "admin", false); + String adminToken = login("admin", "admin", false); // Create the user - Form form = new Form(); - form.param("username", username); - form.param("email", username + "@docs.com"); - form.param("password", "12345678"); - form.param("storage_quota", "1000000"); // 1MB quota resource.path("/user").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminAuthenticationToken) - .put(Entity.form(form), JsonObject.class); + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("username", username) + .param("email", username + "@docs.com") + .param("password", "12345678") + .param("storage_quota", "1000000")), JsonObject.class); // 1MB quota + + // Add to groups + for (String groupName : groupNameList) { + resource.path("/group/" + groupName).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("username", username)), JsonObject.class); + } // Logout admin - logout(adminAuthenticationToken); + logout(adminToken); + } + + /** + * Creates a group. + * + * @param name Name + */ + public void createGroup(String name) { + createGroup(name, null); + } + + /** + * Creates a group. + * + * @param name Name + * @param parent Parent + */ + public void createGroup(String name, String parentId) { + // Login admin to create the group + String adminToken = login("admin", "admin", false); + + // Create the gorup + resource.path("/group").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", name) + .param("parent", parentId)), JsonObject.class); + + // Logout admin + logout(adminToken); } /** 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 index 7d224d54..71afdf40 100644 --- 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 @@ -1,6 +1,7 @@ package com.sismics.docs.rest.resource; import java.text.MessageFormat; +import java.util.List; import javax.json.Json; import javax.json.JsonArrayBuilder; @@ -19,14 +20,16 @@ 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.GroupDao; import com.sismics.docs.core.dao.jpa.UserDao; +import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria; import com.sismics.docs.core.dao.jpa.criteria.UserCriteria; +import com.sismics.docs.core.dao.jpa.dto.GroupDto; 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.Group; 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; @@ -42,27 +45,52 @@ public class AclResource extends BaseResource { /** * Add an ACL. * + * @param sourceId Source ID + * @param permStr Permission + * @param targetName Target name + * @param type ACL type * @return Response */ @PUT public Response add(@FormParam("source") String sourceId, @FormParam("perm") String permStr, - @FormParam("username") String username) { + @FormParam("target") String targetName, + @FormParam("type") String typeStr) { if (!authenticate()) { throw new ForbiddenClientException(); } - // TODO Allow group input // Validate input ValidationUtil.validateRequired(sourceId, "source"); PermType perm = PermType.valueOf(ValidationUtil.validateLength(permStr, "perm", 1, 30, false)); - username = ValidationUtil.validateLength(username, "username", 1, 50, false); + AclTargetType type = AclTargetType.valueOf(ValidationUtil.validateLength(typeStr, "type", 1, 10, false)); + targetName = ValidationUtil.validateLength(targetName, "target", 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)); + // Search user or group + String targetId = null; + switch (type) { + case USER: + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(targetName); + if (user != null) { + targetId = user.getId(); + } + break; + case GROUP: + GroupDao groupDao = new GroupDao(); + Group group = groupDao.getActiveByName(targetName); + if (group != null) { + targetId = group.getId(); + } + break; + case SHARE: + // Share must use the Share REST resource + break; + } + + // Does a target has been found? + if (targetId == null) { + throw new ClientException("InvalidTarget", MessageFormat.format("This target does not exist: {0}", targetName)); } // Check permission on the source by the principal @@ -75,7 +103,7 @@ public class AclResource extends BaseResource { Acl acl = new Acl(); acl.setSourceId(sourceId); acl.setPerm(perm); - acl.setTargetId(user.getId()); + acl.setTargetId(targetId); // Avoid duplicates if (!aclDao.checkPermission(acl.getSourceId(), acl.getPerm(), Lists.newArrayList(acl.getTargetId()))) { @@ -85,8 +113,8 @@ public class AclResource extends BaseResource { JsonObjectBuilder response = Json.createObjectBuilder() .add("perm", acl.getPerm().name()) .add("id", acl.getTargetId()) - .add("name", user.getUsername()) - .add("type", AclTargetType.USER.name()); + .add("name", targetName) + .add("type", type.name()); return Response.ok().entity(response.build()).build(); } @@ -96,7 +124,9 @@ public class AclResource extends BaseResource { /** * Deletes an ACL. * - * @param id ACL ID + * @param sourceId Source ID + * @param permStr Permission + * @param targetId Target ID * @return Response */ @DELETE @@ -155,20 +185,25 @@ public class AclResource extends BaseResource { // Search users UserDao userDao = new UserDao(); JsonArrayBuilder users = Json.createArrayBuilder(); - - PaginatedList paginatedList = PaginatedLists.create(); SortCriteria sortCriteria = new SortCriteria(1, true); - - userDao.findByCriteria(paginatedList, new UserCriteria().setSearch(search), sortCriteria); - for (UserDto userDto : paginatedList.getResultList()) { + List userDtoList = userDao.findByCriteria(new UserCriteria().setSearch(search), sortCriteria); + for (UserDto userDto : userDtoList) { users.add(Json.createObjectBuilder() - .add("username", userDto.getUsername())); + .add("name", userDto.getUsername())); } - // TODO Returns groups too + // Search groups + GroupDao groupDao = new GroupDao(); + JsonArrayBuilder groups = Json.createArrayBuilder(); + List groupDtoList = groupDao.findByCriteria(new GroupCriteria().setSearch(search), sortCriteria); + for (GroupDto groupDto : groupDtoList) { + groups.add(Json.createObjectBuilder() + .add("name", groupDto.getName())); + } JsonObjectBuilder response = Json.createObjectBuilder() - .add("users", users); + .add("users", users) + .add("groups", groups); return Response.ok().entity(response.build()).build(); } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java index 7a3789f2..f7e607a0 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java @@ -87,7 +87,7 @@ public abstract class BaseResource { * @return List of ACL target ID */ protected List getTargetIdList(String shareId) { - List targetIdList = Lists.newArrayList(principal.getGroupIdList()); + List targetIdList = Lists.newArrayList(principal.getGroupIdSet()); if (principal.getId() != null) { targetIdList.add(principal.getId()); } 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 092eb3d7..0f9d21de 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 @@ -149,7 +149,7 @@ public class DocumentResource extends BaseResource { if (!principal.isAnonymous() && (aclDto.getTargetId().equals(principal.getId()) - || principal.getGroupIdList().contains(aclDto.getTargetId())) + || principal.getGroupIdSet().contains(aclDto.getTargetId())) && aclDto.getPerm() == PermType.WRITE) { // The document is writable for the current user writable = true; diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java index dda9f424..956f29df 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java @@ -1,17 +1,26 @@ package com.sismics.docs.rest.resource; import java.text.MessageFormat; +import java.util.List; import javax.json.Json; import javax.json.JsonObjectBuilder; +import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.PUT; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import com.google.common.base.Strings; import com.sismics.docs.core.dao.jpa.GroupDao; +import com.sismics.docs.core.dao.jpa.UserDao; +import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria; +import com.sismics.docs.core.dao.jpa.dto.GroupDto; import com.sismics.docs.core.model.jpa.Group; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.model.jpa.UserGroup; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; @@ -39,10 +48,11 @@ public class GroupResource extends BaseResource { // Validate input name = ValidationUtil.validateLength(name, "name", 1, 50, false); + ValidationUtil.validateAlphanumeric(name, "name"); // Avoid duplicates GroupDao groupDao = new GroupDao(); - Group existingGroup = groupDao.getByName(name); + Group existingGroup = groupDao.getActiveByName(name); if (existingGroup != null) { throw new ClientException("GroupAlreadyExists", MessageFormat.format("This group already exists: {0}", name)); } @@ -50,9 +60,9 @@ public class GroupResource extends BaseResource { // Validate parent String parentId = null; if (!Strings.isNullOrEmpty(parentName)) { - Group parentGroup = groupDao.getByName(parentName); + Group parentGroup = groupDao.getActiveByName(parentName); if (parentGroup == null) { - throw new ClientException("ParentGroupNotFound", MessageFormat.format("This group doest not exists: {0}", parentName)); + throw new ClientException("ParentGroupNotFound", MessageFormat.format("This group does not exists: {0}", parentName)); } parentId = parentGroup.getId(); } @@ -67,4 +77,104 @@ public class GroupResource extends BaseResource { .add("status", "ok"); return Response.ok().entity(response.build()).build(); } + + /** + * Add a user to a group. + * + * @param groupName Group name + * @param username Username + * @return Response + */ + @PUT + @Path("{groupName: [a-zA-Z0-9_]+}") + public Response addMember(@PathParam("groupName") String groupName, + @FormParam("username") String username) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Validate input + groupName = ValidationUtil.validateLength(groupName, "name", 1, 50, false); + username = ValidationUtil.validateLength(username, "username", 1, 50, false); + + // Get the group + GroupDao groupDao = new GroupDao(); + Group group = groupDao.getActiveByName(groupName); + if (group == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + // Get the user + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(username); + if (user == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + // Avoid duplicates + List groupDtoList = groupDao.findByCriteria(new GroupCriteria().setUserId(user.getId()), null); + boolean found = false; + for (GroupDto groupDto : groupDtoList) { + if (groupDto.getId().equals(group.getId())) { + found = true; + } + } + + if (!found) { + // Add the membership + UserGroup userGroup = new UserGroup(); + userGroup.setGroupId(group.getId()); + userGroup.setUserId(user.getId()); + groupDao.addMember(userGroup); + } + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } + + /** + * Remove an user from a group. + * + * @param groupName Group name + * @param username Username + * @return Response + */ + @DELETE + @Path("{groupName: [a-zA-Z0-9_]+}/{username: [a-zA-Z0-9_]+}") + public Response removeMember(@PathParam("groupName") String groupName, + @PathParam("username") String username) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Validate input + groupName = ValidationUtil.validateLength(groupName, "name", 1, 50, false); + username = ValidationUtil.validateLength(username, "username", 1, 50, false); + + // Get the group + GroupDao groupDao = new GroupDao(); + Group group = groupDao.getActiveByName(groupName); + if (group == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + // Get the user + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(username); + if (user == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + // Remove the membership + groupDao.removeMember(group.getId(), user.getId()); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } } 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 c406aa57..93957469 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 @@ -37,6 +37,7 @@ public class ShareResource extends BaseResource { * Add a share to a document. * * @param documentId Document ID + * @param name Share name * @return Response */ @PUT 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 779c2ab1..b00d1059 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 @@ -29,9 +29,12 @@ import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao; import com.sismics.docs.core.dao.jpa.DocumentDao; import com.sismics.docs.core.dao.jpa.FileDao; +import com.sismics.docs.core.dao.jpa.GroupDao; import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao; import com.sismics.docs.core.dao.jpa.UserDao; +import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria; import com.sismics.docs.core.dao.jpa.criteria.UserCriteria; +import com.sismics.docs.core.dao.jpa.dto.GroupDto; import com.sismics.docs.core.dao.jpa.dto.UserDto; import com.sismics.docs.core.event.DocumentDeletedAsyncEvent; import com.sismics.docs.core.event.FileDeletedAsyncEvent; @@ -41,8 +44,6 @@ import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.EncryptionUtil; -import com.sismics.docs.core.util.jpa.PaginatedList; -import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ClientException; @@ -299,14 +300,7 @@ public class UserResource extends BaseResource { } // Get the value of the session token - String authToken = null; - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if (TokenBasedSecurityFilter.COOKIE_NAME.equals(cookie.getName())) { - authToken = cookie.getValue(); - } - } - } + String authToken = getAuthToken(); AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); AuthenticationToken authenticationToken = null; @@ -457,18 +451,39 @@ public class UserResource extends BaseResource { response.add("is_default_password", Constants.DEFAULT_ADMIN_PASSWORD.equals(adminUser.getPassword())); } } else { + // Update the last connection date + String authToken = getAuthToken(); + AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); + authenticationTokenDao.updateLastConnectionDate(authToken); + + // Build the response response.add("anonymous", false); UserDao userDao = new UserDao(); + GroupDao groupDao = new GroupDao(); User user = userDao.getById(principal.getId()); + List groupDtoList = groupDao.findByCriteria(new GroupCriteria() + .setUserId(user.getId()) + .setRecursive(true), null); + response.add("username", user.getUsername()) .add("email", user.getEmail()) .add("storage_quota", user.getStorageQuota()) .add("storage_current", user.getStorageCurrent()); + + // Base functions JsonArrayBuilder baseFunctions = Json.createArrayBuilder(); for (String baseFunction : ((UserPrincipal) principal).getBaseFunctionSet()) { baseFunctions.add(baseFunction); } + + // Groups + JsonArrayBuilder groups = Json.createArrayBuilder(); + for (GroupDto groupDto : groupDtoList) { + groups.add(groupDto.getName()); + } + response.add("base_functions", baseFunctions) + .add("groups", groups) .add("is_default_password", hasBaseFunction(BaseFunction.ADMIN) && Constants.DEFAULT_ADMIN_PASSWORD.equals(user.getPassword())); } @@ -495,8 +510,19 @@ public class UserResource extends BaseResource { throw new ClientException("UserNotFound", "The user doesn't exist"); } + // Groups + GroupDao groupDao = new GroupDao(); + List groupDtoList = groupDao.findByCriteria( + new GroupCriteria().setUserId(user.getId()), + new SortCriteria(1, true)); + JsonArrayBuilder groups = Json.createArrayBuilder(); + for (GroupDto groupDto : groupDtoList) { + groups.add(groupDto.getName()); + } + JsonObjectBuilder response = Json.createObjectBuilder() .add("username", user.getUsername()) + .add("groups", groups) .add("email", user.getEmail()) .add("storage_quota", user.getStorageQuota()) .add("storage_current", user.getStorageCurrent()); @@ -515,8 +541,6 @@ public class UserResource extends BaseResource { @GET @Path("list") public Response list( - @QueryParam("limit") Integer limit, - @QueryParam("offset") Integer offset, @QueryParam("sort_column") Integer sortColumn, @QueryParam("asc") Boolean asc) { if (!authenticate()) { @@ -524,12 +548,11 @@ public class UserResource extends BaseResource { } JsonArrayBuilder users = Json.createArrayBuilder(); - PaginatedList paginatedList = PaginatedLists.create(limit, offset); SortCriteria sortCriteria = new SortCriteria(sortColumn, asc); UserDao userDao = new UserDao(); - userDao.findByCriteria(paginatedList, new UserCriteria(), sortCriteria); - for (UserDto userDto : paginatedList.getResultList()) { + List userDtoList = userDao.findByCriteria(new UserCriteria(), sortCriteria); + for (UserDto userDto : userDtoList) { users.add(Json.createObjectBuilder() .add("id", userDto.getId()) .add("username", userDto.getUsername()) @@ -540,7 +563,6 @@ public class UserResource extends BaseResource { } JsonObjectBuilder response = Json.createObjectBuilder() - .add("total", paginatedList.getResultCount()) .add("users", users); return Response.ok().entity(response.build()).build(); } @@ -558,14 +580,7 @@ public class UserResource extends BaseResource { } // Get the value of the session token - String authToken = null; - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if (TokenBasedSecurityFilter.COOKIE_NAME.equals(cookie.getName())) { - authToken = cookie.getValue(); - } - } - } + String authToken = getAuthToken(); JsonArrayBuilder sessions = Json.createArrayBuilder(); AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); @@ -600,14 +615,7 @@ public class UserResource extends BaseResource { } // Get the value of the session token - String authToken = null; - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if (TokenBasedSecurityFilter.COOKIE_NAME.equals(cookie.getName())) { - authToken = cookie.getValue(); - } - } - } + String authToken = getAuthToken(); // Remove other tokens AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); @@ -618,4 +626,20 @@ public class UserResource extends BaseResource { .add("status", "ok"); return Response.ok().entity(response.build()).build(); } + + /** + * Returns the authentication token value. + * + * @return Token value + */ + private String getAuthToken() { + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + if (TokenBasedSecurityFilter.COOKIE_NAME.equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } } diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentViewPermissions.js b/docs-web/src/main/webapp/src/app/docs/controller/DocumentViewPermissions.js index 2c6ecb5f..b1e2d022 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/DocumentViewPermissions.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/DocumentViewPermissions.js @@ -74,7 +74,7 @@ angular.module('docs').controller('DocumentViewPermissions', function ($scope, $ .get({ search: $viewValue }).then(function(data) { - deferred.resolve(_.pluck(data.users, 'username'), true); + deferred.resolve(_.pluck(data.users, 'name'), true); }); return deferred.promise; }; diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsUser.js b/docs-web/src/main/webapp/src/app/docs/controller/SettingsUser.js index 8a499264..4ad78169 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/SettingsUser.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/SettingsUser.js @@ -8,7 +8,7 @@ angular.module('docs').controller('SettingsUser', function($scope, $state, Resta * Load users from server. */ $scope.loadUsers = function() { - Restangular.one('user/list').get({ limit: 100 }).then(function(data) { + Restangular.one('user/list').get().then(function(data) { $scope.users = data.users; }); }; diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html b/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html index c8921061..cd1640bb 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html @@ -22,10 +22,10 @@
- +
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 index aa8bc5cc..30fc9efb 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java @@ -28,15 +28,18 @@ public class TestAclResource extends BaseJerseyTest { */ @Test public void testAclResource() { + // Create aclGroup2 + clientUtil.createGroup("aclGroup2"); + // Login acl1 clientUtil.createUser("acl1"); String acl1Token = clientUtil.login("acl1"); // Login acl2 - clientUtil.createUser("acl2"); + clientUtil.createUser("acl2", "aclGroup2"); String acl2Token = clientUtil.login("acl2"); - // Create a document + // Create a document with acl1 JsonObject json = target().path("/document").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl1Token) .put(Entity.form(new Form() @@ -65,7 +68,8 @@ public class TestAclResource extends BaseJerseyTest { .put(Entity.form(new Form() .param("source", document1Id) .param("perm", "READ") - .param("username", "acl2")), JsonObject.class); + .param("target", "acl2") + .param("type", "USER")), JsonObject.class); String acl2Id = json.getString("id"); // Add an ACL WRITE for acl2 with acl1 @@ -74,7 +78,8 @@ public class TestAclResource extends BaseJerseyTest { .put(Entity.form(new Form() .param("source", document1Id) .param("perm", "WRITE") - .param("username", "acl2")), JsonObject.class); + .param("target", "acl2") + .param("type", "USER")), JsonObject.class); // Add an ACL WRITE for acl2 with acl1 (again) json = target().path("/acl").request() @@ -82,7 +87,27 @@ public class TestAclResource extends BaseJerseyTest { .put(Entity.form(new Form() .param("source", document1Id) .param("perm", "WRITE") - .param("username", "acl2")), JsonObject.class); + .param("target", "acl2") + .param("type", "USER")), JsonObject.class); + + // Add an ACL READ for aclGroup2 with acl1 + json = target().path("/acl").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl1Token) + .put(Entity.form(new Form() + .param("source", document1Id) + .param("perm", "READ") + .param("target", "aclGroup2") + .param("type", "GROUP")), JsonObject.class); + String aclGroup2Id = json.getString("id"); + + // Add an ACL WRITE for aclGroup2 with acl1 + json = target().path("/acl").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl1Token) + .put(Entity.form(new Form() + .param("source", document1Id) + .param("perm", "WRITE") + .param("target", "aclGroup2") + .param("type", "GROUP")), JsonObject.class); // Get the document as acl1 json = target().path("/document/" + document1Id).request() @@ -90,7 +115,8 @@ public class TestAclResource extends BaseJerseyTest { .get(JsonObject.class); Assert.assertEquals(document1Id, json.getString("id")); acls = json.getJsonArray("acls"); - Assert.assertEquals(4, acls.size()); + Assert.assertEquals(6, acls.size()); + Assert.assertTrue(json.getBoolean("writable")); // Get the document as acl2 json = target().path("/document/" + document1Id).request() @@ -98,7 +124,8 @@ public class TestAclResource extends BaseJerseyTest { .get(JsonObject.class); Assert.assertEquals(document1Id, json.getString("id")); acls = json.getJsonArray("acls"); - Assert.assertEquals(4, acls.size()); + Assert.assertEquals(6, acls.size()); + Assert.assertTrue(json.getBoolean("writable")); // Update the document as acl2 json = target().path("/document/" + document1Id).request() @@ -121,6 +148,29 @@ public class TestAclResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) .delete(JsonObject.class); + // Get the document as acl2 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) + .get(JsonObject.class); + Assert.assertEquals(document1Id, json.getString("id")); + acls = json.getJsonArray("acls"); + Assert.assertEquals(5, acls.size()); + Assert.assertTrue(json.getBoolean("writable")); // Writable by aclGroup2 + + // Delete the ACL WRITE for aclGroup2 with acl2 + target().path("/acl/" + document1Id + "/WRITE/" + aclGroup2Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) + .delete(JsonObject.class); + + // Get the document as acl2 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) + .get(JsonObject.class); + Assert.assertEquals(document1Id, json.getString("id")); + acls = json.getJsonArray("acls"); + Assert.assertEquals(4, acls.size()); + Assert.assertFalse(json.getBoolean("writable")); + // Delete the ACL READ for acl2 with acl2 (not authorized) response = target().path("/acl/" + document1Id + "/READ/" + acl2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) @@ -132,6 +182,16 @@ public class TestAclResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl1Token) .delete(JsonObject.class); + // Get the document as acl2 (visible by group) + target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) + .get(JsonObject.class); + + // Delete the ACL READ for aclGroup2 with acl1 + target().path("/acl/" + document1Id + "/READ/" + aclGroup2Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl1Token) + .delete(JsonObject.class); + // Get the document as acl1 json = target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl1Token) @@ -167,5 +227,7 @@ public class TestAclResource extends BaseJerseyTest { .get(JsonObject.class); JsonArray users = json.getJsonArray("users"); Assert.assertEquals(2, users.size()); + JsonArray groups = json.getJsonArray("groups"); + Assert.assertEquals(1, groups.size()); } } \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java index 46f24a3a..2c26250d 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java @@ -1,9 +1,14 @@ package com.sismics.docs.rest; +import java.util.ArrayList; +import java.util.List; + +import javax.json.JsonArray; import javax.json.JsonObject; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; +import org.junit.Assert; import org.junit.Test; import com.sismics.util.filter.TokenBasedSecurityFilter; @@ -25,10 +30,71 @@ public class TestGroupResource extends BaseJerseyTest { // Login admin String adminToken = clientUtil.login("admin", "admin", false); - // Create a group - target().path("/group").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) - .put(Entity.form(new Form() - .param("name", "Group 1")), JsonObject.class); + // Create group hierarchy + clientUtil.createGroup("g1"); + clientUtil.createGroup("g11", "g1"); + clientUtil.createGroup("g12", "g1"); + clientUtil.createGroup("g111", "g11"); + clientUtil.createGroup("g112", "g11"); + + // Login group1 + clientUtil.createUser("group1", "g112", "g12"); + String group1Token = clientUtil.login("group1"); + + // Check group1 groups (all computed groups) + JsonObject json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, group1Token) + .get(JsonObject.class); + JsonArray groups = json.getJsonArray("groups"); + List groupList = new ArrayList<>(); + for (int i = 0; i < groups.size(); i++) { + groupList.add(groups.getString(i)); + } + Assert.assertEquals(4, groups.size()); + Assert.assertTrue(groupList.contains("g1")); + Assert.assertTrue(groupList.contains("g12")); + Assert.assertTrue(groupList.contains("g11")); + Assert.assertTrue(groupList.contains("g112")); + + // Check group1 groups with admin (only direct groups) + json = target().path("/user/group1").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + groups = json.getJsonArray("groups"); + Assert.assertEquals(2, groups.size()); + Assert.assertEquals("g112", groups.getString(0)); + Assert.assertEquals("g12", groups.getString(1)); + + // Add group1 to g112 (again) + json = target().path("/group/g112").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("username", "group1")), JsonObject.class); + + // Check group1 groups (all computed groups) + json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, group1Token) + .get(JsonObject.class); + groups = json.getJsonArray("groups"); + Assert.assertEquals(4, groups.size()); + + // Remove group1 from g12 + json = target().path("/group/g12/group1").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .delete(JsonObject.class); + + // Check group1 groups (all computed groups) + json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, group1Token) + .get(JsonObject.class); + groups = json.getJsonArray("groups"); + groupList = new ArrayList<>(); + for (int i = 0; i < groups.size(); i++) { + groupList.add(groups.getString(i)); + } + Assert.assertEquals(3, groups.size()); + Assert.assertTrue(groupList.contains("g1")); + Assert.assertTrue(groupList.contains("g11")); + Assert.assertTrue(groupList.contains("g112")); } } \ No newline at end of file