Repository: Kinto/kinto

Issue:
We should consider hidding resources permissions when we only have the read permission
This is related to https://github.com/mozilla-services/kinto/pull/88

When we have the `read` permission to list existing buckets, we can ask for a bucket and in that case we can list its permissions. I think it is only something that somebody that can modify this permission should be able to see.

We should consider hidding resources permissions when we only have the read permission
This is related to https://github.com/mozilla-services/kinto/pull/88

When we have the `read` permission to list existing buckets, we can ask for a bucket and in that case we can list its permissions. I think it is only something that somebody that can modify this permission should be able to see.



Discussion:

Resolving patch:
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -1087,6 +1087,7 @@ def __init__(self, *args, **kwargs):
             self.model.current_principal = Everyone
         else:
             self.model.current_principal = self.request.prefixed_userid
+        self.model.effective_principals = self.request.effective_principals
 
         if self.context:
             self.model.get_permission_object_id = functools.partial(
diff --git a/kinto/core/resource/model.py b/kinto/core/resource/model.py
--- a/kinto/core/resource/model.py
+++ b/kinto/core/resource/model.py
@@ -244,6 +244,7 @@ def __init__(self, *args, **kwargs):
         self.get_permission_object_id = None
         # Current user main principal.
         self.current_principal = None
+        self.effective_principals = None
 
     def _allow_write(self, perm_object_id):
         """Helper to give the ``write`` permission to the current user.
@@ -252,6 +253,18 @@ def _allow_write(self, perm_object_id):
                                              'write',
                                              self.current_principal)
 
+    def _annotate(self, record, perm_object_id):
+        permissions = self.permission.get_object_permissions(perm_object_id)
+        # Permissions are not returned if user only has read permission.
+        writers = permissions.get('write', [])
+        principals = self.effective_principals + [self.current_principal]
+        if len(set(writers) & set(principals)) == 0:
+            permissions = {}
+        # Insert the permissions values in the response.
+        annotated = record.copy()
+        annotated[self.permissions_field] = permissions
+        return annotated
+
     def delete_records(self, filters=None, parent_id=None):
         """Delete permissions when collection records are deleted in bulk.
         """
@@ -267,11 +280,8 @@ def get_record(self, record_id, parent_id=None):
         """
         record = super(ShareableModel, self).get_record(record_id, parent_id)
         perm_object_id = self.get_permission_object_id(record_id)
-        permissions = self.permission.get_object_permissions(perm_object_id)
 
-        annotated = record.copy()
-        annotated[self.permissions_field] = permissions
-        return annotated
+        return self._annotate(record, perm_object_id)
 
     def create_record(self, record, parent_id=None):
         """Create record and set specified permissions.
@@ -284,11 +294,8 @@ def create_record(self, record, parent_id=None):
         perm_object_id = self.get_permission_object_id(record_id)
         self.permission.replace_object_permissions(perm_object_id, permissions)
         self._allow_write(perm_object_id)
-        permissions = self.permission.get_object_permissions(perm_object_id)
 
-        annotated = record.copy()
-        annotated[self.permissions_field] = permissions
-        return annotated
+        return self._annotate(record, perm_object_id)
 
     def update_record(self, record, parent_id=None):
         """Update record and the specified permissions.
@@ -304,11 +311,8 @@ def update_record(self, record, parent_id=None):
         perm_object_id = self.get_permission_object_id(record_id)
         self.permission.replace_object_permissions(perm_object_id, permissions)
         self._allow_write(perm_object_id)
-        permissions = self.permission.get_object_permissions(perm_object_id)
 
-        annotated = record.copy()
-        annotated[self.permissions_field] = permissions
-        return annotated
+        return self._annotate(record, perm_object_id)
 
     def delete_record(self, record_id, parent_id=None, last_modified=None):
         """Delete record and its associated permissions.


Test patch:
diff --git a/kinto/core/testing.py b/kinto/core/testing.py
--- a/kinto/core/testing.py
+++ b/kinto/core/testing.py
@@ -32,6 +32,10 @@ def __init__(self, *args, **kwargs):
         self.authenticated_userid = 'bob'
         self.authn_type = 'basicauth'
         self.prefixed_userid = 'basicauth:bob'
+        self.effective_principals = [
+            self.prefixed_userid,
+            'system.Everyone',
+            'system.Authenticated']
         self.json = {}
         self.validated = {}
         self.matchdict = {}
diff --git a/tests/core/resource/test_object_permissions.py b/tests/core/resource/test_object_permissions.py
--- a/tests/core/resource/test_object_permissions.py
+++ b/tests/core/resource/test_object_permissions.py
@@ -40,8 +40,9 @@ def setUp(self):
         record = self.resource.model.create_record({})
         record_id = record['id']
         record_uri = '/articles/%s' % record_id
-        self.permission.add_principal_to_ace(record_uri, 'read', 'fxa:user')
-        self.permission.add_principal_to_ace(record_uri, 'write', 'fxa:user')
+        self.permission.add_principal_to_ace(record_uri, 'read', 'basicauth:bob')
+        self.permission.add_principal_to_ace(record_uri, 'read', 'account:readonly')
+        self.permission.add_principal_to_ace(record_uri, 'write', 'basicauth:bob')
         self.resource.record_id = record_id
         self.resource.request.validated = {'data': {}}
         self.resource.request.path = record_uri
@@ -65,8 +66,14 @@ def test_permissions_are_not_provided_in_record_delete(self):
     def test_permissions_gives_lists_of_principals_per_ace(self):
         result = self.resource.get()
         permissions = result['permissions']
-        self.assertEqual(sorted(permissions['read']), ['fxa:user'])
-        self.assertEqual(sorted(permissions['write']), ['fxa:user'])
+        self.assertEqual(sorted(permissions['read']), ['account:readonly', 'basicauth:bob'])
+        self.assertEqual(sorted(permissions['write']), ['basicauth:bob'])
+
+    def test_permissions_are_hidden_if_user_has_only_read_permission(self):
+        self.resource.model.current_principal = 'account:readonly'
+        self.resource.model.effective_principals = []
+        result = self.resource.get()
+        self.assertEqual(result['permissions'], {})
 
 
 class SpecifyRecordPermissionTest(PermissionTest):
@@ -77,8 +84,7 @@ def setUp(self):
         self.record_uri = '/articles/%s' % record_id
         self.permission.add_principal_to_ace(self.record_uri,
                                              'read',
-                                             'fxa:user')
-        self.resource.model.current_principal = 'basicauth:userid'
+                                             'account:readonly')
         self.resource.record_id = record_id
         self.resource.request.validated = {'data': {}}
         self.resource.request.path = self.record_uri
@@ -88,13 +94,13 @@ def test_write_permission_is_given_to_creator_on_post(self):
         self.resource.request.method = 'POST'
         result = self.resource.collection_post()
         self.assertEqual(sorted(result['permissions']['write']),
-                         ['basicauth:userid'])
+                         ['basicauth:bob'])
 
     def test_write_permission_is_given_to_put(self):
         self.resource.request.method = 'PUT'
         result = self.resource.put()
         permissions = result['permissions']
-        self.assertEqual(sorted(permissions['write']), ['basicauth:userid'])
+        self.assertEqual(sorted(permissions['write']), ['basicauth:bob'])
 
     def test_write_permission_is_given_to_anonymous(self):
         request = self.get_request()
@@ -115,7 +121,7 @@ def test_permissions_can_be_specified_in_collection_post(self):
         self.resource.request.validated = {'data': {}, 'permissions': perms}
         result = self.resource.collection_post()
         self.assertEqual(sorted(result['permissions']['write']),
-                         ['basicauth:userid', 'jean-louis'])
+                         ['basicauth:bob', 'jean-louis'])
 
     def test_permissions_are_replaced_with_put(self):
         perms = {'write': ['jean-louis']}
@@ -132,9 +138,9 @@ def test_permissions_are_modified_with_patch(self):
         self.resource.request.method = 'PATCH'
         result = self.resource.patch()
         permissions = result['permissions']
-        self.assertEqual(sorted(permissions['read']), ['fxa:user'])
+        self.assertEqual(sorted(permissions['read']), ['account:readonly'])
         self.assertEqual(sorted(permissions['write']),
-                         ['basicauth:userid', 'jean-louis'])
+                         ['basicauth:bob', 'jean-louis'])
 
     def test_permissions_can_be_removed_with_patch_but_keep_current_user(self):
         self.permission.add_principal_to_ace(self.record_uri,
@@ -146,8 +152,8 @@ def test_permissions_can_be_removed_with_patch_but_keep_current_user(self):
         self.resource.request.method = 'PATCH'
         result = self.resource.patch()
         permissions = result['permissions']
-        self.assertEqual(sorted(permissions['read']), ['fxa:user']),
-        self.assertEqual(sorted(permissions['write']), ['basicauth:userid'])
+        self.assertEqual(sorted(permissions['read']), ['account:readonly']),
+        self.assertEqual(sorted(permissions['write']), ['basicauth:bob'])
 
     def test_permissions_can_be_removed_with_patch(self):
         self.permission.add_principal_to_ace(self.record_uri,
@@ -160,7 +166,7 @@ def test_permissions_can_be_removed_with_patch(self):
         result = self.resource.patch()
         self.assertNotIn('read', result['permissions'])
         self.assertEqual(sorted(result['permissions']['write']),
-                         ['basicauth:userid', 'jean-louis'])
+                         ['basicauth:bob', 'jean-louis'])
 
     def test_412_errors_do_not_put_permission_in_record(self):
         self.resource.request.headers['If-Match'] = '"1234567"'  # invalid
diff --git a/tests/test_views_objects_permissions.py b/tests/test_views_objects_permissions.py
--- a/tests/test_views_objects_permissions.py
+++ b/tests/test_views_objects_permissions.py
@@ -53,6 +53,14 @@ def test_cannot_write_if_not_allowed(self):
                           headers=self.alice_headers,
                           status=403)
 
+    def test_permissions_are_not_returned_if_can_only_read(self):
+        resp = self.app.get('/buckets/sodas', headers=self.alice_headers)
+        self.assertEqual(resp.json['permissions'], {})
+
+    def test_permissions_are_returned_if_can_write(self):
+        resp = self.app.get('/buckets/sodas', headers=self.headers)
+        self.assertIn('write', resp.json['permissions'])
+
 
 class CollectionPermissionsTest(PermissionsTest):