Repository: Kinto/kinto

Issue:
Consistency on creation ? 403 versus 412
If we try to create an object that was already created by someone else, the server returns `403 Unauthorized`.

If we add the header `If-None-Match: *`, it also returns `403` even I have the permission to create new objects (and not `412 Precondition failed`), which can be confusing.
In the documentation we don't make those corner cases very clear.

Is that consistent by the way ? Knowing that we want to prevent Alice to know what objects ids Bob has created if she has no permission to read them.

Consistency on creation ? 403 versus 412
If we try to create an object that was already created by someone else, the server returns `403 Unauthorized`.

If we add the header `If-None-Match: *`, it also returns `403` even I have the permission to create new objects (and not `412 Precondition failed`), which can be confusing.
In the documentation we don't make those corner cases very clear.

Is that consistent by the way ? Knowing that we want to prevent Alice to know what objects ids Bob has created if she has no permission to read them.



Discussion:
>  it also returns 403 even I have the permission to create new objects

This should be only the case for buckets.

Implement a test case where:
- alice creates a bucket with `group:create: [Authenticated]`
- alice creates a group `abc`
- bob cannot read groups of buckets (including `abc`)
- bob tries to create a group `abc` with `If-None-Match: *`. It should return `412` and not `403` since he is authorized to create groups.

Implement another test case:
- alice creates a bucket with `write: [Authenticated]`
- alice creates a group `abc`
- bob tries to create a group `abc` with `If-None-Match: *`. It should return `412` since he is authorized to create groups

Implement another test case:
- alice creates a bucket with `read: [Authenticated]`
- alice creates a group `abc`
- bob tries to create a group `abc` with `If-None-Match: *`. It should return `403`since he is not authorized to create groups

>  it also returns 403 even I have the permission to create new objects

This should be only the case for buckets.

Implement a test case where:
- alice creates a bucket with `group:create: [Authenticated]`
- alice creates a group `abc`
- bob cannot read groups of buckets (including `abc`)
- bob tries to create a group `abc` with `If-None-Match: *`. It should return `412` and not `403` since he is authorized to create groups.

Implement another test case:
- alice creates a bucket with `write: [Authenticated]`
- alice creates a group `abc`
- bob tries to create a group `abc` with `If-None-Match: *`. It should return `412` since he is authorized to create groups

Implement another test case:
- alice creates a bucket with `read: [Authenticated]`
- alice creates a group `abc`
- bob tries to create a group `abc` with `If-None-Match: *`. It should return `403`since he is not authorized to create groups


Resolving patch:
diff --git a/kinto/core/authorization.py b/kinto/core/authorization.py
--- a/kinto/core/authorization.py
+++ b/kinto/core/authorization.py
@@ -221,6 +221,9 @@ def _find_required_permission(self, request, service):
         method = request.method.lower()
         required_permission = self.method_permissions.get(method)
 
+        # For create permission, the object id is the plural endpoint.
+        collection_path = service.collection_path.format(**request.matchdict)
+
         # In the case of a "PUT", check if the targetted record already
         # exists, return "write" if it does, "create" otherwise.
         if request.method.lower() == "put":
@@ -233,10 +236,15 @@ def _find_required_permission(self, request, service):
             except storage_exceptions.RecordNotFoundError:
                 # The record does not exist, the permission to create on
                 # the related collection is required.
-                permission_object_id = service.collection_path.format(
-                    **request.matchdict)
+                permission_object_id = collection_path
                 required_permission = "create"
             else:
-                required_permission = "write"
+                # For safe creations, the user needs a create permission.
+                # See Kinto/kinto#792
+                if request.headers.get('If-None-Match') == '*':
+                    permission_object_id = collection_path
+                    required_permission = "create"
+                else:
+                    required_permission = "write"
 
         return (permission_object_id, required_permission)


Test patch:
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
@@ -183,3 +183,56 @@ def test_record_permissions_are_modified_by_patch(self):
                                    {'permissions': {'read': ['fxa:user']}},
                                    headers=self.headers)
         self.assertIn('fxa:user', resp.json['permissions']['read'])
+
+
+class ChildrenCreationTest(PermissionsTest):
+    def setUp(self):
+        self.app.put_json('/buckets/create',
+                          {'permissions': {'group:create': ['system.Authenticated']}},
+                          headers=self.alice_headers)
+        self.app.put_json('/buckets/write',
+                          {'permissions': {'write': ['system.Authenticated']}},
+                          headers=self.alice_headers)
+        self.app.put_json('/buckets/read',
+                          {'permissions': {'read': ['system.Authenticated']}},
+                          headers=self.alice_headers)
+        for parent in ('create', 'write', 'read'):
+            self.app.put_json('/buckets/%s/groups/child' % parent,
+                              MINIMALIST_GROUP,
+                              headers=self.alice_headers)
+        self.bob_headers_safe_creation = dict({'If-None-Match': '*'},
+                                              **self.bob_headers)
+
+    def test_cannot_read_others_objects_if_only_allowed_to_create(self):
+        self.app.get('/buckets/create/groups', headers=self.bob_headers, status=403)
+        self.app.get('/buckets/create/groups/child', headers=self.bob_headers, status=403)
+
+    def test_safe_creation_with_put_returns_412_if_allowed_to_create(self):
+        self.app.put_json('/buckets/create/groups/child',
+                          MINIMALIST_GROUP,
+                          headers=self.bob_headers_safe_creation, status=412)
+
+    def test_safe_creation_with_post_returns_412_if_allowed_to_create(self):
+        self.app.post_json('/buckets/create/groups',
+                           {'data': {'id': 'child', 'members': []}},
+                           headers=self.bob_headers_safe_creation, status=412)
+
+    def test_safe_creation_with_put_returns_412_if_allowed_to_write(self):
+        self.app.put_json('/buckets/write/groups/child',
+                          MINIMALIST_GROUP,
+                          headers=self.bob_headers_safe_creation, status=412)
+
+    def test_safe_creation_with_post_returns_412_if_allowed_to_write(self):
+        self.app.post_json('/buckets/write/groups',
+                           {'data': {'id': 'child', 'members': []}},
+                           headers=self.bob_headers_safe_creation, status=412)
+
+    def test_safe_creation_with_put_returns_403_if_only_allowed_to_read(self):
+        self.app.put_json('/buckets/read/groups/child',
+                          MINIMALIST_GROUP,
+                          headers=self.bob_headers_safe_creation, status=403)
+
+    def test_safe_creation_with_post_returns_403_if_only_allowed_to_read(self):
+        self.app.post_json('/buckets/read/groups',
+                           {'data': {'id': 'child', 'members': []}},
+                           headers=self.bob_headers_safe_creation, status=403)