diff --git a/seahub/api2/endpoints/be_shared_repo.py b/seahub/api2/endpoints/be_shared_repo.py index efeffbb1f0..0a4311f839 100644 --- a/seahub/api2/endpoints/be_shared_repo.py +++ b/seahub/api2/endpoints/be_shared_repo.py @@ -12,11 +12,12 @@ from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error from seahub.utils import is_valid_username, is_org_context +from seahub.share.models import ExtraSharePermission json_content_type = 'application/json; charset=utf-8' class BeSharedRepo(APIView): - authentication_classes = (TokenAuthentication, SessionAuthentication ) + authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated,) throttle_classes = (UserRateThrottle, ) @@ -42,6 +43,10 @@ class BeSharedRepo(APIView): else: seaserv.remove_share(repo_id, from_email, username) + # Delete data of ExtraSharePermission table. + ExtraSharePermission.objects.delete_share_permission(repo_id, + username) + elif share_type == 'group': from_email = request.GET.get('from', None) diff --git a/seahub/api2/endpoints/dir_shared_items.py b/seahub/api2/endpoints/dir_shared_items.py index 76cfaafc88..619d0b1d35 100644 --- a/seahub/api2/endpoints/dir_shared_items.py +++ b/seahub/api2/endpoints/dir_shared_items.py @@ -23,15 +23,19 @@ from seahub.api2.endpoints.utils import is_org_user from seahub.base.templatetags.seahub_tags import email2nickname from seahub.base.accounts import User +from seahub.share.models import ExtraSharePermission from seahub.share.signals import share_repo_to_user_successful, \ share_repo_to_group_successful from seahub.utils import (is_org_context, is_valid_username, send_perm_audit_msg) +from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE, \ + PERMISSION_ADMIN logger = logging.getLogger(__name__) json_content_type = 'application/json; charset=utf-8' + class DirSharedItemsEndpoint(APIView): """Support uniform interface(list, share, unshare, modify) for sharing library/folder to users/groups. @@ -41,9 +45,10 @@ class DirSharedItemsEndpoint(APIView): throttle_classes = (UserRateThrottle,) def list_user_shared_items(self, request, repo_id, path): - username = request.user.username if is_org_context(request): + # when calling seafile API to share authority related functions, change the uesrname to repo owner. + username = seafile_api.get_org_repo_owner(repo_id) org_id = request.user.org.org_id if path == '/': share_items = seafile_api.list_org_repo_shared_to(org_id, @@ -52,11 +57,15 @@ class DirSharedItemsEndpoint(APIView): share_items = seafile_api.get_org_shared_users_for_subdir(org_id, repo_id, path, username) else: + username = seafile_api.get_repo_owner(repo_id) if path == '/': share_items = seafile_api.list_repo_shared_to(username, repo_id) else: share_items = seafile_api.get_shared_users_for_subdir(repo_id, path, username) + + # change is_admin to True if user is repo admin. + admin_users = ExtraSharePermission.objects.get_admin_users_by_repo(repo_id) ret = [] for item in share_items: ret.append({ @@ -66,6 +75,7 @@ class DirSharedItemsEndpoint(APIView): "nickname": email2nickname(item.user), }, "permission": item.perm, + "is_admin": item.user in admin_users }) return ret @@ -179,11 +189,8 @@ class DirSharedItemsEndpoint(APIView): if seafile_api.get_dir_id_by_path(repo.id, path) is None: return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) - if username != self.get_repo_owner(request, repo_id): - return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') - - permission = request.data.get('permission', 'r') - if permission not in ['r', 'rw']: + permission = request.data.get('permission', PERMISSION_READ) + if permission not in [PERMISSION_READ, PERMISSION_READ_WRITE, PERMISSION_ADMIN]: return api_error(status.HTTP_400_BAD_REQUEST, 'permission invalid.') shared_to_user, shared_to_group = self.handle_shared_to_args(request) @@ -192,12 +199,26 @@ class DirSharedItemsEndpoint(APIView): if shared_to is None or not is_valid_username(shared_to): return api_error(status.HTTP_400_BAD_REQUEST, 'Email %s invalid.' % shared_to) + if username != self.get_repo_owner(request, repo_id) and \ + ExtraSharePermission.objects.get_user_permission(repo_id, username) != PERMISSION_ADMIN: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + else: + if username != self.get_repo_owner(request, repo_id): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + if shared_to_user: try: User.objects.get(email=shared_to) except User.DoesNotExist: return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid user, should be registered') + extra_share_permission = "" + if permission not in [PERMISSION_READ, PERMISSION_READ_WRITE]: + extra_share_permission = permission + permission = PERMISSION_READ_WRITE if permission == PERMISSION_ADMIN else PERMISSION_READ + if is_org_context(request): + username = seafile_api.get_org_repo_owner(repo_id) org_id = request.user.org.org_id if path == '/': seafile_api.org_set_share_permission( @@ -206,6 +227,7 @@ class DirSharedItemsEndpoint(APIView): seafile_api.org_update_share_subdir_perm_for_user( org_id, repo_id, path, username, shared_to, permission) else: + username = seafile_api.get_repo_owner(repo_id) if path == '/': seafile_api.set_share_permission( repo_id, username, shared_to, permission) @@ -213,6 +235,10 @@ class DirSharedItemsEndpoint(APIView): seafile_api.update_share_subdir_perm_for_user( repo_id, path, username, shared_to, permission) + if path == '/': + ExtraSharePermission.objects.update_share_permission(repo_id, + shared_to, + extra_share_permission) send_perm_audit_msg('modify-repo-perm', username, shared_to, repo_id, path, permission) @@ -257,15 +283,20 @@ class DirSharedItemsEndpoint(APIView): if seafile_api.get_dir_id_by_path(repo.id, path) is None: return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) - if username != self.get_repo_owner(request, repo_id): - return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') - share_type = request.data.get('share_type') if share_type != 'user' and share_type != 'group': return api_error(status.HTTP_400_BAD_REQUEST, 'share_type invalid.') - permission = request.data.get('permission', 'r') - if permission not in ['r', 'rw']: + if share_type == 'user': + if username != self.get_repo_owner(request, repo_id) and \ + ExtraSharePermission.objects.get_user_permission(repo_id, username) != PERMISSION_ADMIN: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + else: + if username != self.get_repo_owner(request, repo_id): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + permission = request.data.get('permission', PERMISSION_READ) + if permission not in [PERMISSION_READ, PERMISSION_READ_WRITE, PERMISSION_ADMIN]: return api_error(status.HTTP_400_BAD_REQUEST, 'permission invalid.') result = {} @@ -299,7 +330,13 @@ class DirSharedItemsEndpoint(APIView): continue try: + extra_share_permission = '' + if permission == PERMISSION_ADMIN: + extra_share_permission = permission + permission = PERMISSION_READ_WRITE + if is_org_context(request): + username = seafile_api.get_org_repo_owner(repo_id) org_id = request.user.org.org_id if not is_org_user(to_user, int(org_id)): @@ -330,6 +367,7 @@ class DirSharedItemsEndpoint(APIView): }) continue + username = seafile_api.get_repo_owner(repo_id) if path == '/': seafile_api.share_repo( repo_id, username, to_user, permission) @@ -337,6 +375,8 @@ class DirSharedItemsEndpoint(APIView): sub_repo_id = seafile_api.share_subdir_to_user( repo_id, path, username, to_user, permission) + if path == '/' and extra_share_permission == PERMISSION_ADMIN: + ExtraSharePermission.objects.create_share_permission(repo_id, to_user, extra_share_permission) # send a signal when sharing repo successful if path == '/': share_repo_to_user_successful.send(sender=None, @@ -352,7 +392,8 @@ class DirSharedItemsEndpoint(APIView): "name": to_user, "nickname": email2nickname(to_user), }, - "permission": permission + "permission": permission, + "is_admin": extra_share_permission == PERMISSION_ADMIN }) send_perm_audit_msg('add-repo-perm', username, to_user, @@ -455,20 +496,27 @@ class DirSharedItemsEndpoint(APIView): if seafile_api.get_dir_id_by_path(repo.id, path) is None: return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) - if username != self.get_repo_owner(request, repo_id): - return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') - + # check permission shared_to_user, shared_to_group = self.handle_shared_to_args(request) if shared_to_user: shared_to = request.GET.get('username') if shared_to is None or not is_valid_username(shared_to): return api_error(status.HTTP_400_BAD_REQUEST, 'Email %s invalid.' % shared_to) + if username != self.get_repo_owner(request, repo_id) and \ + ExtraSharePermission.objects.get_user_permission(repo_id, username) != PERMISSION_ADMIN: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + else: + if username != self.get_repo_owner(request, repo_id): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + if shared_to_user: # if user not found, permission will be None permission = seafile_api.check_permission_by_path( repo_id, '/', shared_to) if is_org_context(request): + username = seafile_api.get_org_repo_owner(repo_id) org_id = request.user.org.org_id if path == '/': seaserv.seafserv_threaded_rpc.org_remove_share( @@ -478,12 +526,17 @@ class DirSharedItemsEndpoint(APIView): org_id, repo_id, path, username, shared_to) else: + username = seafile_api.get_repo_owner(repo_id) if path == '/': seaserv.remove_share(repo_id, username, shared_to) else: seafile_api.unshare_subdir_for_user( repo_id, path, username, shared_to) + # Delete share permission at ExtraSharePermission table. + if path == '/': + ExtraSharePermission.objects.delete_share_permission(repo_id, + shared_to) send_perm_audit_msg('delete-repo-perm', username, shared_to, repo_id, path, permission) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index c5705d4b7c..42773c4ef8 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -46,6 +46,7 @@ from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \ grp_avatar from seahub.base.accounts import User from seahub.base.models import UserStarredFiles, DeviceToken +from seahub.share.models import ExtraSharePermission from seahub.base.templatetags.seahub_tags import email2nickname, \ translate_seahub_time, translate_commit_desc_escape, \ email2contact_email @@ -485,6 +486,8 @@ class Repos(APIView): else: shared_repos = seafile_api.get_share_in_repo_list( email, -1, -1) + repos_with_admin_share_to = ExtraSharePermission.objects.\ + get_repos_with_admin_permission(email) # Reduce memcache fetch ops. owners_set = set([x.user for x in shared_repos]) @@ -518,6 +521,13 @@ class Repos(APIView): "head_commit_id": r.head_cmmt_id, "version": r.version, } + + + if r.repo_id in repos_with_admin_share_to: + repo['is_admin'] = True + else: + repo['is_admin'] = False + repos_json.append(repo) if filter_by['group']: diff --git a/seahub/constants.py b/seahub/constants.py index 3c76773d83..688aaebb2d 100644 --- a/seahub/constants.py +++ b/seahub/constants.py @@ -4,3 +4,8 @@ DEFAULT_USER = 'default' # Guest user have limited operations, can not create group and library. GUEST_USER = 'guest' + +# Permissions +PERMISSION_READ = 'r' +PERMISSION_READ_WRITE = 'rw' +PERMISSION_ADMIN = 'admin' diff --git a/seahub/share/models.py b/seahub/share/models.py index 27e8e46a4b..546c01f4a4 100644 --- a/seahub/share/models.py +++ b/seahub/share/models.py @@ -1,8 +1,10 @@ # Copyright (c) 2012-2016 Seafile Ltd. +import operator import datetime import logging from django.db import models +from django.db.models import Q from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.hashers import make_password, check_password @@ -11,6 +13,7 @@ from constance import config from seahub.base.fields import LowerCaseCharField from seahub.utils import normalize_file_path, normalize_dir_path, gen_token,\ get_service_url +from seahub.constants import PERMISSION_READ, PERMISSION_ADMIN # Get an instance of a logger logger = logging.getLogger(__name__) @@ -150,6 +153,79 @@ class FileShareManager(models.Manager): def get_valid_dir_link_by_token(self, token): return self._get_valid_file_share_by_token(token) + +class ExtraSharePermissionManager(models.Manager): + def get_user_permission(self, repo_id, username): + """Get user's permission of a library. + return + e.g. 'admin' + """ + record_list = super(ExtraSharePermissionManager, self).filter( + repo_id=repo_id, share_to=username + ) + if len(record_list) > 0: + return record_list[0].permission + else: + return None + + def get_repos_with_admin_permission(self, username): + """Get repo id list a user has admin permission. + """ + shared_repos = super(ExtraSharePermissionManager, self).filter( + share_to=username, permission=PERMISSION_ADMIN + ) + return [e.repo_id for e in shared_repos] + + def get_admin_users_by_repo(self, repo_id): + """Gets the share and permissions of the record in the specified repo ID. + return + e.g. ['admin_user1', 'admin_user2'] + """ + shared_repos = super(ExtraSharePermissionManager, self).filter( + repo_id=repo_id, permission=PERMISSION_ADMIN + ) + + return [e.share_to for e in shared_repos] + + def batch_is_admin(self, in_datas): + """return the data that input data is admin + e.g. + in_datas: + [(repo_id1, username1), (repo_id2, admin1)] + return: + [(repo_id2, admin1)] + """ + if len(in_datas) <= 0: + return [] + query = reduce( + operator.or_, + (Q(repo_id=data[0], share_to=data[1]) for data in in_datas) + ) + db_data = super(ExtraSharePermissionManager, self).filter(query).filter(permission=PERMISSION_ADMIN) + return [(e.repo_id, e.share_to) for e in db_data] + + def create_share_permission(self, repo_id, username, permission): + self.model(repo_id=repo_id, share_to=username, + permission=permission).save() + + def delete_share_permission(self, repo_id, share_to): + super(ExtraSharePermissionManager, self).filter(repo_id=repo_id, + share_to=share_to).delete() + + def update_share_permission(self, repo_id, share_to, permission): + super(ExtraSharePermissionManager, self).filter(repo_id=repo_id, + share_to=share_to).delete() + if permission in [PERMISSION_ADMIN]: + self.create_share_permission(repo_id, share_to, permission) + + +class ExtraSharePermission(models.Model): + repo_id = models.CharField(max_length=36, db_index=True) + share_to = models.CharField(max_length=255, db_index=True) + permission = models.CharField(max_length=30) + objects = ExtraSharePermissionManager() + + class FileShare(models.Model): """ Model used for file or dir shared link. @@ -315,7 +391,7 @@ class PrivateFileDirShareManager(models.Manager): """ """ return self.add_private_file_share(from_user, to_user, repo_id, - path, 'r') + path, PERMISSION_READ) def get_private_share_in_file(self, username, repo_id, path): """Get a file that private shared to ``username``. diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 0db6236946..168fc511d9 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -898,7 +898,7 @@ <% if (user_perm == 'rw' && !repo_encrypted && can_generate_upload_link) { %>