Эх сурвалжийг харах

[doc2] Add recursive share permissions to directory and add tests

Jenny Kim 9 жил өмнө
parent
commit
46abde6

+ 14 - 9
desktop/core/src/desktop/api2.py

@@ -122,8 +122,7 @@ def get_document(request):
   else:
     doc = Document2.objects.get_by_uuid(uuid=request.GET['uuid'])
 
-  doc_info = doc.to_dict()
-  return JsonResponse(doc_info)
+  return JsonResponse(doc.to_dict())
 
 
 @api_error_handler
@@ -137,9 +136,12 @@ def move_document(request):
 
   source = Directory.objects.get_by_uuid(uuid=source_doc_uuid)
   destination = Directory.objects.get_by_uuid(uuid=destination_doc_uuid)
-  source.move(destination, request.user)
+  doc = source.move(destination, request.user)
 
-  return JsonResponse({'status': 0})
+  return JsonResponse({
+    'status': 0,
+    'document': doc.to_dict()
+  })
 
 
 @api_error_handler
@@ -200,13 +202,13 @@ def share_document(request):
   """
   Set who else or which other group can interact with the document.
 
-  Example of input: {'read': {'user_ids': [1, 2, 3], 'group_ids': [1, 2, 3]}}
+  Example of input: {'read': {'user_ids': [1, 2, 3], 'group_ids': [1, 2, 3], 'all': false}}
   """
   perms_dict = json.loads(request.POST.get('data'))
   uuid = json.loads(request.POST.get('uuid'))
 
-  if not uuid:
-    raise PopupException(_('share_document requires uuid'))
+  if not uuid or not perms_dict:
+    raise PopupException(_('share_document requires uuid and perms_dict'))
 
   doc = Document2.objects.get_by_uuid(uuid=uuid)
 
@@ -222,10 +224,13 @@ def share_document(request):
     else:
       groups = []
 
-    doc.share(request.user, name=name, users=users, groups=groups)
+    all = perm.get('all', False)
+
+    doc = doc.share(request.user, name=name, users=users, groups=groups, all=all)
 
   return JsonResponse({
-      'status': 0,
+    'status': 0,
+    'document': doc.to_dict()
   })
 
 

+ 60 - 30
desktop/core/src/desktop/models.py

@@ -940,7 +940,7 @@ class Document2(models.Model):
       return reverse('oozie:edit_workflow') + '?workflow=' + str(self.id)
 
   def to_dict(self):
-    doc_dict = {
+    return {
       'owner': self.owner.username,
       'name': self.name,
       'path': urlencode(self.path or '/'),
@@ -955,14 +955,28 @@ class Document2(models.Model):
       'isSelected': False,
       'absoluteUrl': self.get_absolute_url()
     }
-    return doc_dict
+
+  def can_read(self, user):
+    has_read_permissions = False
+    perm = self.list_permissions('read')
+    if perm:
+      has_read_permissions = perm.groups.filter(id__in=user.groups.all()).exists() or user in perm.users.all()
+    return user.is_superuser or self.owner == user or self.can_write(user) or has_read_permissions
 
   def can_read_or_exception(self, user):
-    self.doc.get().can_read_or_exception(user)
+    if self.doc.get():
+      self.doc.get().can_read_or_exception(user)
+    elif self.can_read(user):
+      return True
+    else:
+      raise PopupException(_("Document does not exist or you don't have the permission to access it."))
 
   def can_write(self, user):
+    has_write_permissions = False
     perm = self.list_permissions('write')
-    return user.is_superuser or self.owner == user or perm.groups.filter(id__in=user.groups.all()).exists() or user in perm.users.all()
+    if perm:
+      has_write_permissions = perm.groups.filter(id__in=user.groups.all()).exists() or user in perm.users.all()
+    return user.is_superuser or self.owner == user or has_write_permissions
 
   def can_write_or_exception(self, user):
     if self.can_write(user):
@@ -1022,7 +1036,6 @@ class Document2(models.Model):
        Document2.objects.filter(name=self.name, owner=self.owner, type='directory').exists():
       raise FilesystemException(_('Cannot create or modify the home or .Trash directory.'))
 
-
   def move(self, directory, user):
     if not directory.is_directory:
       raise FilesystemException(_('Target with UUID %s is not a directory') % directory.uuid)
@@ -1031,6 +1044,8 @@ class Document2(models.Model):
       self.parent_directory = directory
       self.save()
 
+    return self
+
   def trash(self):
     trash_dir = Directory.objects.get(name=self.TRASH_DIR, owner=self.owner)
     self.move(trash_dir, self.owner)
@@ -1039,49 +1054,55 @@ class Document2(models.Model):
 
   # TODO: restore
 
-  def share(self, user, name='read', users=None, groups=None):
+  def share(self, user, name='read', users=None, groups=None, all=False):
+    with transaction.atomic():
+      self.update_permission(user, name, users, groups, all)
+      # For directories, update all children recursively with same permissions
+      for child in self.children.all():
+        child.share(user, name, users, groups, all)
+    return self
+
+  def update_permission(self, user, name='read', users=None, groups=None, all=False):
     # TODO check in settings if user can sync, re-share, which perms...
 
     perm, created = Document2Permission.objects.get_or_create(doc=self, perms=name)
 
+    perm.users = []
     if users is not None:
-      perm.users = []
       perm.users = users
-      perm.save()
 
+    perm.groups = []
     if groups is not None:
-      perm.groups = []
       perm.groups = groups
-      perm.save()
 
-    if not users and not groups:
-      perm.delete()
+    perm.all = all
+
+    perm.save()
 
   def list_permissions(self, perm='read'):
-    # FIXME This is causing an integrity error b/c it is called by doc2 api potentially concurrently for new docs
-    perm, created = Document2Permission.objects.get_or_create(doc=self, perms=perm)
-    return perm
+    try:
+      return Document2Permission.objects.get(doc=self, perms=perm)
+    except Document2Permission.DoesNotExist:
+      return None
 
   def _massage_permissions(self):
     """
     Returns the permissions for a given document as a dictionary
     """
+    permissions = {
+      'read': {'users': [], 'groups': [], 'all': False},
+      'write': {'users': [], 'groups': [], 'all': False}
+    }
+
     read_perms = self.list_permissions(perm='read')
     write_perms = self.list_permissions(perm='write')
-    return {
-      'read': {
-        'users': [{'id': perm_user.id, 'username': perm_user.username} \
-                  for perm_user in read_perms.users.all()],
-        'groups': [{'id': perm_group.id, 'name': perm_group.name} \
-                   for perm_group in read_perms.groups.all()]
-      },
-      'write': {
-        'users': [{'id': perm_user.id, 'username': perm_user.username} \
-                  for perm_user in write_perms.users.all()],
-        'groups': [{'id': perm_group.id, 'name': perm_group.name} \
-                   for perm_group in write_perms.groups.all()]
-      }
-    }
+
+    if read_perms:
+      permissions.update(read_perms.to_dict())
+    if write_perms:
+      permissions.update(write_perms.to_dict())
+
+    return permissions
 
   def _redact_query(self):
     """
@@ -1156,7 +1177,7 @@ class Document2Permission(models.Model):
 
   users = models.ManyToManyField(auth_models.User, db_index=True, db_table='documentpermission2_users')
   groups = models.ManyToManyField(auth_models.Group, db_index=True, db_table='documentpermission2_groups')
-  all = models.BooleanField(db_index=True, default=True, help_text=_t('Specify users/groups or ALL'))
+  all = models.BooleanField(db_index=True, default=False, help_text=_t('Specify users/groups or ALL'))
 
   perms = models.CharField(default=READ_PERM, max_length=10, db_index=True, choices=( # one perm
     (READ_PERM, 'read'),
@@ -1170,6 +1191,15 @@ class Document2Permission(models.Model):
   class Meta:
     unique_together = ('doc', 'perms')
 
+  def to_dict(self):
+    return {
+      self.perms: {
+        'users': [{'id': perm_user.id, 'username': perm_user.username} for perm_user in self.users.all()],
+        'groups': [{'id': perm_group.id, 'name': perm_group.name} for perm_group in self.groups.all()],
+        'all': self.all
+      }
+    }
+
 
 def get_data_link(meta):
   link = None

+ 212 - 2
desktop/core/src/desktop/tests_doc2.py

@@ -27,6 +27,7 @@ from desktop.models import import_saved_beeswax_query, Directory, Document2
 
 from beeswax.models import SavedQuery
 from beeswax.design import hql_query
+from useradmin.models import get_default_user_group
 
 
 class TestDocument2(object):
@@ -83,6 +84,15 @@ class TestDocument2(object):
       old_query.delete()
 
 
+  def test_get_document(self):
+    doc = Document2.objects.create(name='test_doc', type='query-hive', owner=self.user, data={})
+    self.home_dir.children.add(doc)
+    response = self.client.get('/desktop/api2/doc/get', {'uuid': doc.uuid})
+    data = json.loads(response.content)
+    assert_true('uuid' in data)
+    assert_equal(doc.uuid, data['uuid'])
+
+
   def test_directory_create(self):
     response = self.client.post('/desktop/api2/doc/mkdir', {'parent_uuid': json.dumps(self.home_dir.uuid), 'name': json.dumps('test_mkdir')})
     data = json.loads(response.content)
@@ -187,7 +197,7 @@ class TestDocument2(object):
     assert_equal(Document2.TRASH_DIR, data['children'][0]['name'])
 
 
-  def test_validations(self):
+  def test_validate_name(self):
     # Test invalid names
     invalid_name = '/invalid'
     response = self.client.post('/desktop/api2/doc/mkdir', {'parent_uuid': json.dumps(self.home_dir.uuid), 'name': json.dumps(invalid_name)})
@@ -195,6 +205,8 @@ class TestDocument2(object):
     assert_equal(-1, data['status'], data)
     assert_true('invalid character' in data['message'])
 
+
+  def test_validate_same_name_and_parent(self):
     # Test error on creating documents with same name and location
     test_dir = Directory.objects.create(name='test_dir', owner=self.user, parent_directory=self.home_dir)
     response = self.client.post('/desktop/api2/doc/mkdir', {'parent_uuid': json.dumps(self.home_dir.uuid), 'name': json.dumps('test_dir')})
@@ -207,7 +219,10 @@ class TestDocument2(object):
     data = json.loads(response.content)
     assert_equal(0, data['status'], data)
 
+
+  def test_validate_immutable_user_directories(self):
     # Test that home and Trash directories cannot be recreated or modified
+    test_dir = Directory.objects.create(name='test_dir', owner=self.user, parent_directory=self.home_dir)
     response = self.client.post('/desktop/api2/doc/mkdir', {'parent_uuid': json.dumps(test_dir.uuid), 'name': json.dumps(Document2.TRASH_DIR)})
     data = json.loads(response.content)
     assert_equal(-1, data['status'], data)
@@ -228,4 +243,199 @@ class TestDocument2(object):
     })
     data = json.loads(response.content)
     assert_equal(-1, data['status'], data)
-    assert_equal('Cannot create or modify the home or .Trash directory.', data['message'])
+    assert_equal('Cannot create or modify the home or .Trash directory.', data['message'])
+
+
+class TestDocument2Permissions(object):
+
+  def setUp(self):
+    self.client = make_logged_in_client(username="perm_user", groupname="default", recreate=True, is_superuser=False)
+    self.client_not_me = make_logged_in_client(username="not_perm_user", groupname="default", recreate=True, is_superuser=False)
+
+    self.user = User.objects.get(username="perm_user")
+    self.user_not_me = User.objects.get(username="not_perm_user")
+
+    grant_access(self.user.username, self.user.username, "desktop")
+    grant_access(self.user_not_me.username, self.user_not_me.username, "desktop")
+
+    self.default_group = get_default_user_group()
+
+    # This creates the user directories for the new user
+    response = self.client.get('/desktop/api2/docs/')
+    data = json.loads(response.content)
+    assert_equal('/', data['document']['path'], data)
+
+    self.home_dir = Document2.objects.get_home_directory(user=self.user)
+
+
+  def test_default_permissions(self):
+    # Tests that for a new doc by default, read/write perms are set to no users, groups, or all
+    new_doc = Document2.objects.create(name='new_doc', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+
+    response = self.client.get('/desktop/api2/doc/get', {'uuid': new_doc.uuid})
+    data = json.loads(response.content)
+    assert_equal(new_doc.uuid, data['uuid'], data)
+    assert_true('perms' in data)
+    assert_equal({'read': {'users': [], 'groups': [], 'all': False}, 'write': {'users': [], 'groups': [], 'all': False}},
+                 data['perms'])
+
+
+  def test_share_document_read_by_user(self):
+    doc = Document2.objects.create(name='new_doc', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+
+    # Share read perm by users
+    response = self.client.post("/desktop/api2/doc/share", {
+      'uuid': json.dumps(doc.uuid),
+      'data': json.dumps({
+        'read': {
+          'user_ids': [
+            self.user.id,
+            self.user_not_me.id
+          ],
+          'group_ids': [],
+          'all': False
+        },
+        'write': {
+          'user_ids': [],
+          'group_ids': [],
+          'all': False
+        }
+      })
+    })
+
+    assert_equal(0, json.loads(response.content)['status'], response.content)
+    assert_true(doc.can_read(self.user))
+    assert_true(doc.can_write(self.user))
+    assert_true(doc.can_read(self.user_not_me))
+    assert_false(doc.can_write(self.user_not_me))
+
+
+  def test_share_document_read_by_group(self):
+    doc = Document2.objects.create(name='new_doc', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+
+    response = self.client.post("/desktop/api2/doc/share", {
+      'uuid': json.dumps(doc.uuid),
+      'data': json.dumps({
+        'read': {
+          'user_ids': [
+            self.user.id
+          ],
+          'group_ids': [
+            self.default_group.id
+          ]
+        },
+        'write': {
+          'user_ids': [],
+          'group_ids': []
+        }
+      })
+    })
+
+    assert_equal(0, json.loads(response.content)['status'], response.content)
+    assert_true(doc.can_read(self.user))
+    assert_true(doc.can_write(self.user))
+    assert_true(doc.can_read(self.user_not_me))
+    assert_false(doc.can_write(self.user_not_me))
+
+
+  def test_share_document_write_by_user(self):
+    doc = Document2.objects.create(name='new_doc', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+
+    # Share write perm by user
+    response = self.client.post("/desktop/api2/doc/share", {
+      'uuid': json.dumps(doc.uuid),
+      'data': json.dumps({
+        'read': {
+          'user_ids': [
+            self.user.id
+          ],
+          'group_ids': []
+        },
+        'write': {
+          'user_ids': [
+            self.user_not_me.id
+          ],
+          'group_ids': []
+        }
+      })
+    })
+
+    assert_equal(0, json.loads(response.content)['status'], response.content)
+    assert_true(doc.can_read(self.user))
+    assert_true(doc.can_write(self.user))
+    assert_true(doc.can_read(self.user_not_me))
+    assert_true(doc.can_write(self.user_not_me))
+
+
+  def test_share_document_write_by_group(self):
+    doc = Document2.objects.create(name='new_doc', type='query-hive', owner=self.user, data={}, parent_directory=self.home_dir)
+
+    # Share write perm by group
+    response = self.client.post("/desktop/api2/doc/share", {
+      'uuid': json.dumps(doc.uuid),
+      'data': json.dumps({
+        'read': {
+          'user_ids': [
+            self.user.id
+          ],
+          'group_ids': []
+        },
+        'write': {
+          'user_ids': [],
+          'group_ids': [
+            self.default_group.id
+          ]
+        }
+      })
+    })
+
+    assert_equal(0, json.loads(response.content)['status'], response.content)
+    assert_true(doc.can_read(self.user))
+    assert_true(doc.can_write(self.user))
+    assert_true(doc.can_read(self.user_not_me))
+    assert_true(doc.can_write(self.user_not_me))
+
+
+  def test_share_directory(self):
+    # Test that updating the permissions for a directory updates all nested documents accordingly, with file structure:
+    # /
+    # /test_dir
+    # /test_dir/query1.sql
+    # /test_dir/nested_dir
+    # /test_dir/nested_dir/query2.sql
+
+    # All initially owned by self.user
+    parent_dir = Directory.objects.create(name='test_dir', owner=self.user, parent_directory=self.home_dir)
+    child_doc = Document2.objects.create(name='query1.sql', type='query-hive', owner=self.user, data={}, parent_directory=parent_dir)
+    nested_dir = Directory.objects.create(name='nested_dir', owner=self.user, parent_directory=parent_dir)
+    nested_doc = Document2.objects.create(name='query2.sql', type='query-hive', owner=self.user, data={}, parent_directory=nested_dir)
+
+    for doc in [parent_dir, child_doc, nested_dir, nested_doc]:
+      assert_true(doc.can_read(self.user))
+      assert_true(doc.can_write(self.user))
+      assert_false(doc.can_read(self.user_not_me))
+      assert_false(doc.can_write(self.user_not_me))
+
+    # Update parent_dir permissions to grant write permissions to default group
+    response = self.client.post("/desktop/api2/doc/share", {
+        'uuid': json.dumps(parent_dir.uuid),
+        'data': json.dumps({
+          'read': {
+            'user_ids': [],
+            'group_ids': []
+          },
+          'write': {
+            'user_ids': [],
+            'group_ids': [
+              self.default_group.id
+            ]
+          }
+        })
+    })
+
+    assert_equal(0, json.loads(response.content)['status'], response.content)
+    for doc in [parent_dir, child_doc, nested_dir, nested_doc]:
+      assert_true(doc.can_read(self.user))
+      assert_true(doc.can_write(self.user))
+      assert_true(doc.can_read(self.user_not_me))
+      assert_true(doc.can_write(self.user_not_me))