Repository: Kinto/kinto

Issue:
Add an endpoint to list objects and permissions of current user 
Context: The kinto-admin where we need to render a tree of buckets and collections the authenticated user has access to.

If you don't have read permission to the bucket containing a collection you have read access for, you can't see that bucket listed anywhere it seems. In an ideal world, I'd expect that if I have access to `/buckets/buck/collections/coll`:
- I could query `/buckets` and see `buck` listed;
- I could query `/buckets/buck/collections` and see `coll` listed.

Silly thoughts: 
- Maybe we need a new `enumerate` permission type?
- Maybe we need a new endpoint (possibly through a plugin) allowing to retrieve the tree of available resources for a given authenticated user? eg.

```
$ http http://kinto/v1/access
{
  "buckets": [
    {
      id: "buck",
      permissions: {
        write: [...], read: [...]
      },
      collections: [
        {
          id: "coll",
          permissions: {
            write: [...], read: [...]
          }
        },
        ...
      ]
    },
    ...
  ]
}
```

If that's not possible for whatever reason, how should we retrieve the list of resources an authenticated user has access to?

Note that another possible answer might be that this is just matter of configuring all this manually, by setting permissions individually per resource in the tree; though that would mean giving read permission to containers we don't actually want to fully expose to the user. 



Discussion:
This can probably be considered as a know limitation/bug. And we should fix it.

> considered as a know limitation/**bug**

I don't think it's a bug. A limitation yes. See #442 for a related discussion.

Indeed, with our current permission tree, if the client has not the permission to read the parent object, it cannot see its attributes. Although it can obtain a list of the children (full or partial) that it can read/write.

> Maybe we need a new enumerate permission type?

What would be the difference with read?

> allowing to retrieve the tree of available resources for a given authenticated user

That sounds acceptable and useful ! The only detail is that we would not be able to obtain attributes of parent object for which we have no read permission.

```
$ http http://kinto/v1/access
{
  "buckets": [
    {
      url: "/buckets/buck",
      data: {
          id: "buck",
      },
      permissions: {
        write: [...], read: [...]
      },
      collections: [],
   }, {
      url: "/buckets/buck",
      collections:  [{
          id: "coll",
          permissions: {
            write: [...], read: [...]
          }
        },
        ...
      ]
    },
    ...
  ]
}
```

> > Maybe we need a new enumerate permission type?
> > What would be the difference with read?

Basically allowing to list an entry id but not to access its data.

> The only detail is that we would not be able to obtain attributes of parent object for which we have no read permission.

Well, couldn't we easily infer the nested structure here? If we know the url for a collection we have access to, we know the bucket name it's attached to; so we can build the tree. Am I missing smthg?

Note that we could totally avoid listing the permissions in this tree nodes if we're not allowed to access them.

Edit: if that's really complicated I agree having at least the list of available resources for the user would be still tremendously useful ;)

Actually an export of the permission backend will be useful: a tree of `{parentURI, URI, id}`

One idea: `GET /permissions --auth user:pass`

``` js
{
    buckets: [
        {
            id: "certificates",
            uri: "/buckets/certificates",
            permissions: ["read", "write", "collection:create"]
        },
        {
            id: "blog",
            uri: "/buckets/blog",
            permissions: []
            collections: [
                {
                    id: "articles",
                    uri: "/buckets/blog/collections/articles",
                    permissions: [],
                    records: [
                        {
                            id: "3c6cd52c-75ba-46da-8bf3-d658ddb204a0",
                            uri: "/buckets/blog/collections/articles/records/3c6cd52c-75ba-46da-8bf3-d658ddb204a0",
                            permissions: ["read"]
                        }
                    ]
                },
                {
                    id: "comment",
                    uri: "/buckets/blog/collections/comments",
                    permissions: ["record:create"],
                    records: [
                        {
                            id: "f6b4efaf-4cea-4fae-99b8-07fa876aee92",
                            uri: "/buckets/blog/collections/articles/records/f6b4efaf-4cea-4fae-99b8-07fa876aee92",
                            permissions: ["read", "write"]
                        }
                    ]
                }
            ]
        }
    ]
}
```

Cons:
- Can dump millions of entries (Idea: strip children if permission on parent)
- Query to permission backend can be resource greedy
- Must explicit whole list of permission (eg. `write` gives `read` and `<child>:create`

Looks nice, though I'd limit this to buckets and collections, and maybe add an option for records with a big warning regarding perfs in the docs :)

After discussing with @n1k0, we realized that it would not very convenient to have it as a tree.

Something flat like this would do it:

```
GET /permissions

{
    "data": [
        {
            "uri": "/buckets/toto",
            "resource": "bucket",
            "bucket_id": "toto",
            "permissions": ["read", "write"]
        },
        {
            "uri": "/buckets/tata",
            "resource": "bucket",
            "bucket_id": "tata",
            "permissions": ["collection:create"]
        },
        {
            "uri": "/buckets/toto/collections/test",
            "resource": "collection",
            "bucket_id": "toto",
            "collection_id": "test",
            "permissions": ["record:create"]
        },
        {
            "uri": "/buckets/toto/collections/test/records/d5db6e57-2c10-43e2-96c8-56602ef01435",
            "resource": "record",
            "bucket_id": "toto",
            "collection_id": "test",
            "record_id": "d5db6e57-2c10-43e2-96c8-56602ef01435"
            "permissions": ["write"]
        }
    ]
}
```

In a second step, to reduce the load, it could even be filtrable:
- _For bucket and collection only_ :  `/permissions?in_resource=bucket,collection`
- _Every writable record in this bucket_: `/permissions?bucket_id=test&permission=write&resource=record`

What do you think?

I love it!

Yes it looks good to me.


Resolving patch:
diff --git a/kinto/__init__.py b/kinto/__init__.py
--- a/kinto/__init__.py
+++ b/kinto/__init__.py
@@ -28,7 +28,8 @@
     'bucket_create_principals': Authenticated,
     'multiauth.authorization_policy': (
         'kinto.authorization.AuthorizationPolicy'),
-    'experimental_collection_schema_validation': 'False',
+    'experimental_collection_schema_validation': False,
+    'experimental_permissions_endpoint': False,
     'http_api_version': HTTP_API_VERSION
 }
 
@@ -60,7 +61,7 @@ def main(global_config, config=None, **settings):
     # Scan Kinto views.
     kwargs = {}
 
-    flush_enabled = asbool(settings.get('flush_endpoint_enabled'))
+    flush_enabled = asbool(settings['flush_endpoint_enabled'])
     if flush_enabled:
         config.add_api_capability(
             "flush_endpoint",
@@ -69,7 +70,14 @@ def main(global_config, config=None, **settings):
             url="http://kinto.readthedocs.io/en/latest/configuration/"
                 "settings.html#activating-the-flush-endpoint")
     else:
-        kwargs['ignore'] = 'kinto.views.flush'
+        kwargs['ignore'] = ['kinto.views.flush']
+
+    # Permissions endpoint enabled if permission backend is setup.
+    permissions_endpoint_enabled = (
+        asbool(settings['experimental_permissions_endpoint']) and
+        hasattr(config.registry, 'permission'))
+    if not permissions_endpoint_enabled:
+        kwargs.setdefault('ignore', []).append('kinto.views.permissions')
 
     config.scan("kinto.views", **kwargs)
 
diff --git a/kinto/core/utils.py b/kinto/core/utils.py
--- a/kinto/core/utils.py
+++ b/kinto/core/utils.py
@@ -36,6 +36,7 @@ def json_serializer(v, **kw):
     sqlalchemy = None
 
 from pyramid import httpexceptions
+from pyramid.interfaces import IRoutesMapper
 from pyramid.request import Request, apply_request_extensions
 from pyramid.settings import aslist
 from pyramid.view import render_view_to_response
@@ -360,3 +361,25 @@ def strip_uri_prefix(path):
     Remove potential version prefix in URI.
     """
     return re.sub(r'^(/v\d+)?', '', six.text_type(path))
+
+
+def view_lookup(request, uri):
+    """
+    Look-up the specified `uri` and return the associated resource name
+    along the match dict.
+
+    :param request: the current request (used to obtain registry).
+    :param uri: a plural or object endpoint URI.
+    :rtype: tuple
+    :returns: the resource name and the associated matchdict.
+    """
+    api_prefix = '/%s' % request.upath_info.split('/')[1]
+    q = request.registry.queryUtility
+    routes_mapper = q(IRoutesMapper)
+
+    fakerequest = Request.blank(path=api_prefix + uri)
+    info = routes_mapper(fakerequest)
+    matchdict, route = info['match'], info['route']
+    resource_name = route.name.replace('-record', '')\
+                              .replace('-collection', '')
+    return resource_name, matchdict
diff --git a/kinto/views/permissions.py b/kinto/views/permissions.py
new file mode 100644
--- /dev/null
+++ b/kinto/views/permissions.py
@@ -0,0 +1,68 @@
+from pyramid.security import NO_PERMISSION_REQUIRED, Authenticated
+
+from kinto.authorization import PERMISSIONS_INHERITANCE_TREE
+from kinto.core import Service, utils as core_utils
+
+
+permissions = Service(name='permissions',
+                      description='List of user permissions',
+                      path='/permissions')
+
+
+@permissions.get(permission=NO_PERMISSION_REQUIRED)
+def permissions_get(request):
+    # Invert the permissions inheritance tree.
+    perms_descending_tree = {}
+    for obtained, obtained_from in PERMISSIONS_INHERITANCE_TREE.items():
+        on_resource, obtained_perm = obtained.split(':', 1)
+        for from_resource, perms in obtained_from.items():
+            for perm in perms:
+                perms_descending_tree.setdefault(from_resource, {})\
+                                     .setdefault(perm, {})\
+                                     .setdefault(on_resource, set())\
+                                     .add(obtained_perm)
+
+    # Obtain current principals.
+    principals = request.effective_principals
+    if Authenticated in principals:
+        # Since this view does not require any permission (can be used to
+        # obtain public users permissions), we have to add the prefixed userid
+        # among the principals (see :mode:`kinto.core.authentication`)
+        userid = request.prefixed_userid
+        principals.append(userid)
+
+    # Query every possible permission of the current user from backend.
+    # Since there is no "full-list" method, we query for each possible
+    # permission (read, write, group:create, collection:create, record:create).
+    # XXX: could be optimized into one call to backend when needed.
+    possible_perms = set([k.split(':', 1)[1]
+                          for k in PERMISSIONS_INHERITANCE_TREE.keys()])
+    backend = request.registry.permission
+    perms_by_object_uri = {}
+    for perm in possible_perms:
+        object_uris = backend.get_accessible_objects(principals, perm)
+        for object_uri in object_uris:
+            perms_by_object_uri.setdefault(object_uri, []).append(perm)
+
+    entries = []
+    for object_uri, perms in perms_by_object_uri.items():
+        # Obtain associated resource from object URI
+        resource_name, matchdict = core_utils.view_lookup(request, object_uri)
+        # For consistency with events payloads, prefix id with resource name
+        matchdict[resource_name + '_id'] = matchdict.get('id')
+
+        # Expand implicit permissions using descending tree.
+        permissions = set(perms)
+        for perm in perms:
+            obtained = perms_descending_tree[resource_name][perm]
+            # Related to same resource only and not every sub-objects.
+            # (e.g "bucket:write" gives "bucket:read" but not "group:read")
+            permissions |= obtained[resource_name]
+
+        entry = dict(uri=object_uri,
+                     resource_name=resource_name,
+                     permissions=list(permissions),
+                     **matchdict)
+        entries.append(entry)
+
+    return {"data": entries}


Test patch:
diff --git a/kinto/tests/test_views_objects_permissions.py b/kinto/tests/test_views_objects_permissions.py
new file mode 100644
--- /dev/null
+++ b/kinto/tests/test_views_objects_permissions.py
@@ -0,0 +1,180 @@
+from .support import (BaseWebTest, unittest, get_user_headers,
+                      MINIMALIST_BUCKET, MINIMALIST_COLLECTION,
+                      MINIMALIST_GROUP, MINIMALIST_RECORD)
+
+
+class PermissionsTest(BaseWebTest, unittest.TestCase):
+
+    def __init__(self, *args, **kwargs):
+        super(PermissionsTest, self).__init__(*args, **kwargs)
+        self.alice_headers = self.headers.copy()
+        self.alice_headers.update(**get_user_headers('alice'))
+        self.alice_principal = ('basicauth:8df4b22019cc89d0bb679bc51373a9da56a'
+                                '7ae9978c52fbe684510c3d257c855')
+        self.bob_headers = self.headers.copy()
+        self.bob_headers.update(**get_user_headers('bob'))
+        self.bob_principal = ('basicauth:21364d1282b58b3af15a0d619d03122a318be'
+                              'e30c1a5a2d09c3b958e5f7fb71a')
+
+
+class BucketPermissionsTest(PermissionsTest):
+
+    def setUp(self):
+        bucket = MINIMALIST_BUCKET.copy()
+        bucket['permissions'] = {'read': [self.alice_principal]}
+        self.app.put_json('/buckets/sodas',
+                          bucket,
+                          headers=self.headers)
+
+    def test_creation_is_allowed_to_authenticated_by_default(self):
+        self.app.put_json('/buckets/beer',
+                          MINIMALIST_BUCKET,
+                          headers=self.headers)
+
+    def test_current_user_receives_write_permission_on_creation(self):
+        resp = self.app.put_json('/buckets/beer',
+                                 MINIMALIST_BUCKET,
+                                 headers=self.headers)
+        permissions = resp.json['permissions']
+        self.assertIn(self.principal, permissions['write'])
+
+    def test_can_read_if_allowed(self):
+        self.app.get('/buckets/sodas',
+                     headers=self.alice_headers)
+
+    def test_cannot_write_if_not_allowed(self):
+        self.app.put_json('/buckets/sodas',
+                          MINIMALIST_BUCKET,
+                          headers=self.alice_headers,
+                          status=403)
+
+
+class CollectionPermissionsTest(PermissionsTest):
+
+    def setUp(self):
+        bucket = MINIMALIST_BUCKET.copy()
+        bucket['permissions'] = {
+            'read': [self.alice_principal],
+            'write': [self.bob_principal]
+        }
+        self.app.put_json('/buckets/beer',
+                          bucket,
+                          headers=self.headers)
+        self.app.put_json('/buckets/beer/collections/barley',
+                          MINIMALIST_COLLECTION,
+                          headers=self.headers)
+
+    def test_read_is_allowed_if_read_on_bucket(self):
+        self.app.get('/buckets/beer/collections/barley',
+                     headers=self.alice_headers)
+
+    def test_read_is_allowed_if_write_on_bucket(self):
+        self.app.get('/buckets/beer/collections/barley',
+                     headers=self.bob_headers)
+
+    def test_cannot_read_if_not_allowed(self):
+        headers = self.headers.copy()
+        headers.update(**get_user_headers('jean-louis'))
+        self.app.get('/buckets/beer/collections/barley',
+                     headers=headers,
+                     status=403)
+
+    def test_cannot_write_if_not_allowed(self):
+        self.app.put_json('/buckets/beer/collections/barley',
+                          MINIMALIST_COLLECTION,
+                          headers=self.alice_headers,
+                          status=403)
+
+
+class GroupPermissionsTest(PermissionsTest):
+
+    def setUp(self):
+        bucket = MINIMALIST_BUCKET.copy()
+        bucket['permissions'] = {
+            'read': [self.alice_principal],
+            'write': [self.bob_principal]
+        }
+        self.app.put_json('/buckets/beer',
+                          bucket,
+                          headers=self.headers)
+
+        self.app.put_json('/buckets/beer/groups/moderators',
+                          MINIMALIST_GROUP,
+                          headers=self.headers)
+
+    def test_creation_is_allowed_if_write_on_bucket(self):
+        self.app.post_json('/buckets/beer/groups',
+                           MINIMALIST_GROUP,
+                           headers=self.headers)
+
+    def test_read_is_allowed_if_read_on_bucket(self):
+        self.app.get('/buckets/beer/groups/moderators',
+                     headers=self.alice_headers)
+
+    def test_read_is_allowed_if_write_on_bucket(self):
+        self.app.get('/buckets/beer/groups/moderators',
+                     headers=self.bob_headers)
+
+    def test_cannot_read_if_not_allowed(self):
+        headers = self.headers.copy()
+        headers.update(**get_user_headers('jean-louis'))
+        self.app.get('/buckets/beer/groups/moderators',
+                     headers=headers,
+                     status=403)
+
+    def test_cannot_write_if_not_allowed(self):
+        self.app.put_json('/buckets/beer/groups/moderators',
+                          MINIMALIST_GROUP,
+                          headers=self.alice_headers,
+                          status=403)
+
+    def test_creation_is_forbidden_is_no_write_on_bucket(self):
+        self.app.post_json('/buckets/beer/groups',
+                           MINIMALIST_GROUP,
+                           headers=self.alice_headers,
+                           status=403)
+
+
+class RecordPermissionsTest(PermissionsTest):
+
+    def setUp(self):
+        bucket = MINIMALIST_BUCKET.copy()
+        bucket['permissions'] = {'write': [self.alice_principal]}
+        self.app.put_json('/buckets/beer',
+                          bucket,
+                          headers=self.headers)
+
+        collection = MINIMALIST_COLLECTION.copy()
+        collection['permissions'] = {'write': [self.bob_principal]}
+        self.app.put_json('/buckets/beer/collections/barley',
+                          collection,
+                          headers=self.headers)
+
+    def test_creation_is_allowed_if_write_on_bucket(self):
+        self.app.post_json('/buckets/beer/collections/barley/records',
+                           MINIMALIST_RECORD,
+                           headers=self.alice_headers)
+
+    def test_creation_is_allowed_if_write_on_collection(self):
+        self.app.post_json('/buckets/beer/collections/barley/records',
+                           MINIMALIST_RECORD,
+                           headers=self.bob_headers)
+
+    def test_creation_is_forbidden_is_no_write_on_bucket_nor_collection(self):
+        headers = self.headers.copy()
+        headers.update(**get_user_headers('jean-louis'))
+        self.app.post_json('/buckets/beer/collections/barley/records',
+                           MINIMALIST_RECORD,
+                           headers=headers,
+                           status=403)
+
+    def test_record_permissions_are_modified_by_patch(self):
+        collection_url = '/buckets/beer/collections/barley/records'
+        resp = self.app.post_json(collection_url,
+                                  MINIMALIST_RECORD,
+                                  headers=self.headers)
+        record = resp.json['data']
+        resp = self.app.patch_json(collection_url + '/' + record['id'],
+                                   {'permissions': {'read': ['fxa:user']}},
+                                   headers=self.headers)
+        self.assertIn('fxa:user', resp.json['permissions']['read'])
diff --git a/kinto/tests/test_views_permissions.py b/kinto/tests/test_views_permissions.py
--- a/kinto/tests/test_views_permissions.py
+++ b/kinto/tests/test_views_permissions.py
@@ -1,180 +1,66 @@
-from .support import (BaseWebTest, unittest, get_user_headers,
-                      MINIMALIST_BUCKET, MINIMALIST_COLLECTION,
-                      MINIMALIST_GROUP, MINIMALIST_RECORD)
+from .support import (BaseWebTest, unittest, MINIMALIST_RECORD,
+                      MINIMALIST_GROUP, MINIMALIST_BUCKET,
+                      MINIMALIST_COLLECTION, get_user_headers)
 
 
-class PermissionsTest(BaseWebTest, unittest.TestCase):
+RECORD_ID = 'd5db6e57-2c10-43e2-96c8-56602ef01435'
 
-    def __init__(self, *args, **kwargs):
-        super(PermissionsTest, self).__init__(*args, **kwargs)
-        self.alice_headers = self.headers.copy()
-        self.alice_headers.update(**get_user_headers('alice'))
-        self.alice_principal = ('basicauth:8df4b22019cc89d0bb679bc51373a9da56a'
-                                '7ae9978c52fbe684510c3d257c855')
-        self.bob_headers = self.headers.copy()
-        self.bob_headers.update(**get_user_headers('bob'))
-        self.bob_principal = ('basicauth:21364d1282b58b3af15a0d619d03122a318be'
-                              'e30c1a5a2d09c3b958e5f7fb71a')
 
-
-class BucketPermissionsTest(PermissionsTest):
-
-    def setUp(self):
-        bucket = MINIMALIST_BUCKET.copy()
-        bucket['permissions'] = {'read': [self.alice_principal]}
-        self.app.put_json('/buckets/sodas',
-                          bucket,
-                          headers=self.headers)
-
-    def test_creation_is_allowed_to_authenticated_by_default(self):
-        self.app.put_json('/buckets/beer',
-                          MINIMALIST_BUCKET,
-                          headers=self.headers)
-
-    def test_current_user_receives_write_permission_on_creation(self):
-        resp = self.app.put_json('/buckets/beer',
-                                 MINIMALIST_BUCKET,
-                                 headers=self.headers)
-        permissions = resp.json['permissions']
-        self.assertIn(self.principal, permissions['write'])
-
-    def test_can_read_if_allowed(self):
-        self.app.get('/buckets/sodas',
-                     headers=self.alice_headers)
-
-    def test_cannot_write_if_not_allowed(self):
-        self.app.put_json('/buckets/sodas',
-                          MINIMALIST_BUCKET,
-                          headers=self.alice_headers,
-                          status=403)
-
-
-class CollectionPermissionsTest(PermissionsTest):
+class PermissionsViewTest(BaseWebTest, unittest.TestCase):
 
     def setUp(self):
-        bucket = MINIMALIST_BUCKET.copy()
-        bucket['permissions'] = {
-            'read': [self.alice_principal],
-            'write': [self.bob_principal]
-        }
-        self.app.put_json('/buckets/beer',
-                          bucket,
+        super(PermissionsViewTest, self).setUp()
+        self.app.put_json('/buckets/beers', MINIMALIST_BUCKET,
                           headers=self.headers)
-        self.app.put_json('/buckets/beer/collections/barley',
+        self.app.put_json('/buckets/beers/collections/barley',
                           MINIMALIST_COLLECTION,
                           headers=self.headers)
-
-    def test_read_is_allowed_if_read_on_bucket(self):
-        self.app.get('/buckets/beer/collections/barley',
-                     headers=self.alice_headers)
-
-    def test_read_is_allowed_if_write_on_bucket(self):
-        self.app.get('/buckets/beer/collections/barley',
-                     headers=self.bob_headers)
-
-    def test_cannot_read_if_not_allowed(self):
-        headers = self.headers.copy()
-        headers.update(**get_user_headers('jean-louis'))
-        self.app.get('/buckets/beer/collections/barley',
-                     headers=headers,
-                     status=403)
-
-    def test_cannot_write_if_not_allowed(self):
-        self.app.put_json('/buckets/beer/collections/barley',
-                          MINIMALIST_COLLECTION,
-                          headers=self.alice_headers,
-                          status=403)
-
-
-class GroupPermissionsTest(PermissionsTest):
-
-    def setUp(self):
-        bucket = MINIMALIST_BUCKET.copy()
-        bucket['permissions'] = {
-            'read': [self.alice_principal],
-            'write': [self.bob_principal]
-        }
-        self.app.put_json('/buckets/beer',
-                          bucket,
-                          headers=self.headers)
-
-        self.app.put_json('/buckets/beer/groups/moderators',
+        self.app.put_json('/buckets/beers/groups/amateurs',
                           MINIMALIST_GROUP,
                           headers=self.headers)
-
-    def test_creation_is_allowed_if_write_on_bucket(self):
-        self.app.post_json('/buckets/beer/groups',
-                           MINIMALIST_GROUP,
-                           headers=self.headers)
-
-    def test_read_is_allowed_if_read_on_bucket(self):
-        self.app.get('/buckets/beer/groups/moderators',
-                     headers=self.alice_headers)
-
-    def test_read_is_allowed_if_write_on_bucket(self):
-        self.app.get('/buckets/beer/groups/moderators',
-                     headers=self.bob_headers)
-
-    def test_cannot_read_if_not_allowed(self):
-        headers = self.headers.copy()
-        headers.update(**get_user_headers('jean-louis'))
-        self.app.get('/buckets/beer/groups/moderators',
-                     headers=headers,
-                     status=403)
-
-    def test_cannot_write_if_not_allowed(self):
-        self.app.put_json('/buckets/beer/groups/moderators',
-                          MINIMALIST_GROUP,
-                          headers=self.alice_headers,
-                          status=403)
-
-    def test_creation_is_forbidden_is_no_write_on_bucket(self):
-        self.app.post_json('/buckets/beer/groups',
-                           MINIMALIST_GROUP,
-                           headers=self.alice_headers,
-                           status=403)
-
-
-class RecordPermissionsTest(PermissionsTest):
-
-    def setUp(self):
-        bucket = MINIMALIST_BUCKET.copy()
-        bucket['permissions'] = {'write': [self.alice_principal]}
-        self.app.put_json('/buckets/beer',
-                          bucket,
-                          headers=self.headers)
-
-        collection = MINIMALIST_COLLECTION.copy()
-        collection['permissions'] = {'write': [self.bob_principal]}
-        self.app.put_json('/buckets/beer/collections/barley',
-                          collection,
-                          headers=self.headers)
-
-    def test_creation_is_allowed_if_write_on_bucket(self):
-        self.app.post_json('/buckets/beer/collections/barley/records',
+        self.app.put_json('/buckets/beers/collections/barley/records/' + RECORD_ID,  # noqa
                            MINIMALIST_RECORD,
-                           headers=self.alice_headers)
-
-    def test_creation_is_allowed_if_write_on_collection(self):
-        self.app.post_json('/buckets/beer/collections/barley/records',
-                           MINIMALIST_RECORD,
-                           headers=self.bob_headers)
-
-    def test_creation_is_forbidden_is_no_write_on_bucket_nor_collection(self):
-        headers = self.headers.copy()
-        headers.update(**get_user_headers('jean-louis'))
-        self.app.post_json('/buckets/beer/collections/barley/records',
-                           MINIMALIST_RECORD,
-                           headers=headers,
-                           status=403)
+                           headers=self.headers)
 
-    def test_record_permissions_are_modified_by_patch(self):
-        collection_url = '/buckets/beer/collections/barley/records'
-        resp = self.app.post_json(collection_url,
-                                  MINIMALIST_RECORD,
-                                  headers=self.headers)
-        record = resp.json['data']
-        resp = self.app.patch_json(collection_url + '/' + record['id'],
-                                   {'permissions': {'read': ['fxa:user']}},
-                                   headers=self.headers)
-        self.assertIn('fxa:user', resp.json['permissions']['read'])
+        # Other user.
+        self.app.put_json('/buckets/water', MINIMALIST_BUCKET,
+                          headers=get_user_headers('alice'))
+
+    def get_app_settings(self, additional_settings=None):
+        settings = super(PermissionsViewTest, self).get_app_settings(
+            additional_settings)
+        settings['experimental_permissions_endpoint'] = 'True'
+        return settings
+
+    def test_permissions_list_entries_for_current_principal(self):
+        resp = self.app.get('/permissions', headers=self.headers)
+        permissions = resp.json['data']
+        self.assertEqual(len(permissions), 4)
+
+    def test_permissions_can_be_listed_anonymously(self):
+        self.app.patch_json('/buckets/beers/collections/barley',
+                            {'permissions': {'write': ['system.Everyone']}},
+                            headers=self.headers)
+        resp = self.app.get('/permissions')
+        permissions = resp.json['data']
+        self.assertEqual(len(permissions), 1)
+
+    def test_implicit_permissions_are_explicited(self):
+        resp = self.app.get('/permissions', headers=get_user_headers('alice'))
+        permissions = resp.json['data']
+        bucket_permissions = permissions[0]
+        self.assertEqual(sorted(bucket_permissions['permissions']),
+                         ['collection:create',
+                          'group:create',
+                          'read',
+                          'write'])
+
+    def test_object_details_are_provided(self):
+        resp = self.app.get('/permissions', headers=self.headers)
+        entries = resp.json['data']
+        for entry in entries:
+            if entry['resource_name'] == 'record':
+                record_permission = entry
+        self.assertEqual(record_permission['record_id'], RECORD_ID)
+        self.assertEqual(record_permission['collection_id'], 'barley')
+        self.assertEqual(record_permission['bucket_id'], 'beers')