Repository: Kinto/kinto

Issue:
Batch requests accept additional attributes
We should probably forbid them.

```
> echo '{"requests": [], "foo": {}}' | http post http://localhost:8888/v1/batch 

HTTP/1.1 200 OK
Access-Control-Expose-Headers: Retry-After, Alert, Content-Length, Backoff
Content-Length: 16
Content-Type: application/json
Date: Thu, 16 Mar 2017 23:01:01 GMT
Server: waitress
X-Content-Type-Options: nosniff

{
    "responses": []
}
```

Edit: I just notice we currently test for batch ignoring additional attributes. Is this a feature?


Discussion:
Related to https://github.com/Kinto/kinto-http.py/issues/115
+1 to forbid them explicitly. I wonder why we had for that.

Resolving patch:
diff --git a/kinto/core/views/batch.py b/kinto/core/views/batch.py
--- a/kinto/core/views/batch.py
+++ b/kinto/core/views/batch.py
@@ -42,12 +42,20 @@ class BatchRequestSchema(colander.MappingSchema):
     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),
                                missing=colander.drop)
 
+    @staticmethod
+    def schema_type():
+        return colander.Mapping(unknown='raise')
+
 
 class BatchPayloadSchema(colander.MappingSchema):
     defaults = BatchRequestSchema(missing=colander.drop).clone()
     requests = colander.SchemaNode(colander.Sequence(),
                                    BatchRequestSchema())
 
+    @staticmethod
+    def schema_type():
+        return colander.Mapping(unknown='raise')
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         # On defaults, path is not mandatory.


Test patch:
diff --git a/tests/core/test_views_batch.py b/tests/core/test_views_batch.py
--- a/tests/core/test_views_batch.py
+++ b/tests/core/test_views_batch.py
@@ -175,9 +175,8 @@ def assertInvalid(self, payload):
     def test_requests_is_mandatory(self):
         self.assertInvalid({})
 
-    def test_unknown_attributes_are_dropped(self):
-        deserialized = self.schema.deserialize({'requests': [], 'unknown': 42})
-        self.assertNotIn('unknown', deserialized)
+    def test_raise_invalid_on_unknown_attributes(self):
+        self.assertInvalid({'requests': [], 'unknown': 42})
 
     def test_list_of_requests_can_be_empty(self):
         self.schema.deserialize({'requests': []})
@@ -201,6 +200,10 @@ def test_request_method_must_be_known_uppercase_word(self):
         request = {'path': '/', 'method': 'get'}
         self.assertInvalid({'requests': [request]})
 
+    def test_raise_invalid_on_request_unkown_attributes(self):
+        request = {'path': '/', 'method': 'GET', 'foo': 42}
+        self.assertInvalid({'requests': [request]})
+
     #
     # headers
     #
@@ -247,12 +250,10 @@ def test_defaults_must_be_a_request_schema_if_specified(self):
         batch_payload = {'requests': [request], 'defaults': defaults}
         self.assertInvalid(batch_payload)
 
-    def test_unknown_defaults_are_ignored_silently(self):
+    def test_raise_invalid_on_default_unkown_attributes(self):
         request = {'path': '/'}
         defaults = {'foo': 'bar'}
-        batch_payload = {'requests': [request], 'defaults': defaults}
-        result = self.schema.deserialize(batch_payload)
-        self.assertNotIn('foo', result['requests'][0])
+        self.assertInvalid({'requests': [request], 'defaults': defaults})
 
     def test_defaults_can_be_specified_empty(self):
         request = {'path': '/'}