Repository: Kinto/kinto

Issue:
Unhandled JSON pointer errors
When setting a schema on resources, unresolvable references are not checked upon creation.

When creating a resource that is checked against that schema, a `RefResolutionError` is raised by `jsonschema` and not caught.

For example, creating the schema:

```json
{"data": {
    "schema": {
      "properties": {
        "address": { "$ref": "#/definitions/address" }
      }
    }
  }
}
```

POST data:

```json
{ "data": {
   "address": "seattle"
  }
}
```

This will crash the server with a 500 error and the following message:

```shell
"Unresolvable JSON pointer: %r" % fragment
jsonschema.exceptions.RefResolutionError: Unresolvable JSON pointer: 'definitions/address' 
```

I can work on this, I was just wondering what you all felt the approach should be.

It can either check when creating the schema or upon each invocation of it (i.e., POST data). The only slight trouble I can see with checking and not allowing unresolvable schema is that the schema can be a url, so if it is not resolving at the time of creation, it will fail.

That seems fair to me; does that seem like a reasonable fix?


Unhandled JSON pointer errors
When setting a schema on resources, unresolvable references are not checked upon creation.

When creating a resource that is checked against that schema, a `RefResolutionError` is raised by `jsonschema` and not caught.

For example, creating the schema:

```json
{"data": {
    "schema": {
      "properties": {
        "address": { "$ref": "#/definitions/address" }
      }
    }
  }
}
```

POST data:

```json
{ "data": {
   "address": "seattle"
  }
}
```

This will crash the server with a 500 error and the following message:

```shell
"Unresolvable JSON pointer: %r" % fragment
jsonschema.exceptions.RefResolutionError: Unresolvable JSON pointer: 'definitions/address' 
```

I can work on this, I was just wondering what you all felt the approach should be.

It can either check when creating the schema or upon each invocation of it (i.e., POST data). The only slight trouble I can see with checking and not allowing unresolvable schema is that the schema can be a url, so if it is not resolving at the time of creation, it will fail.

That seems fair to me; does that seem like a reasonable fix?




Discussion:
Yes if a URL is permitted then I think it's OK to fail when checking it.
Yes if a URL is permitted then I think it's OK to fail when checking it.

Resolving patch:
diff --git a/kinto/schema_validation.py b/kinto/schema_validation.py
--- a/kinto/schema_validation.py
+++ b/kinto/schema_validation.py
@@ -1,5 +1,5 @@
 import colander
-from jsonschema import Draft4Validator, ValidationError, SchemaError, validate
+from jsonschema import Draft4Validator, ValidationError, SchemaError, RefResolutionError, validate
 from pyramid.settings import asbool
 
 from kinto.core import utils
@@ -56,6 +56,13 @@ def validate_schema(data, schema, ignore_fields=[]):
             field = e.schema_path[-1]
         e.field = field
         raise e
+    # Raise an error here if a reference in the schema doesn't resolve.
+    # jsonschema doesn't provide schema validation checking upon creation yet,
+    # it must be validated against data.
+    # See https://github.com/Julian/jsonschema/issues/399
+    # For future support https://github.com/Julian/jsonschema/issues/346.
+    except RefResolutionError as e:
+        raise e
 
 
 def validate_from_bucket_schema_or_400(data, resource_name, request, ignore_fields=[]):
@@ -93,3 +100,5 @@ def validate_from_bucket_schema_or_400(data, resource_name, request, ignore_fiel
         validate_schema(data, schema, ignore_fields=ignore_fields)
     except ValidationError as e:
         raise_invalid(request, name=e.field, description=e.message)
+    except RefResolutionError as e:
+        raise_invalid(request, name='schema', description=str(e))
diff --git a/kinto/views/records.py b/kinto/views/records.py
--- a/kinto/views/records.py
+++ b/kinto/views/records.py
@@ -5,7 +5,7 @@
 from kinto.core.errors import raise_invalid
 from kinto.views import object_exists_or_404
 from kinto.schema_validation import (validate_from_bucket_schema_or_400, validate_schema,
-                                     ValidationError)
+                                     ValidationError, RefResolutionError)
 
 
 _parent_path = '/buckets/{{bucket_id}}/collections/{{collection_id}}'
@@ -66,6 +66,8 @@ def process_record(self, new, old=None):
                 validate_schema(new, schema, ignore_fields=internal_fields)
             except ValidationError as e:
                 raise_invalid(self.request, name=e.field, description=e.message)
+            except RefResolutionError as e:
+                raise_invalid(self.request, name='schema', description=str(e))
 
             # Assign the schema version to the record.
             schema_timestamp = self._collection[self.model.modified_field]


Test patch:
diff --git a/tests/test_views_schema_collection.py b/tests/test_views_schema_collection.py
--- a/tests/test_views_schema_collection.py
+++ b/tests/test_views_schema_collection.py
@@ -79,3 +79,24 @@ def test_collections_are_invalid_if_do_not_match_schema(self):
                           {'data': {'displayFields': 42}},
                           headers=self.headers,
                           status=400)
+
+
+SCHEMA_UNRESOLVABLE = {
+    'properties': {
+        'displayFields': {'$ref': '#/definitions/displayFields'}
+    },
+}
+
+
+class CollectionUnresolvableTest(BaseWebTestWithSchema, unittest.TestCase):
+    def setUp(self):
+        super().setUp()
+        resp = self.app.put_json(BUCKET_URL,
+                                 {'data': {'collection:schema': SCHEMA_UNRESOLVABLE}},
+                                 headers=self.headers)
+        self.collection = resp.json['data']
+
+    def test_unresolvable_errors_handled(self):
+        self.app.put_json(COLLECTION_URL,
+                          {'data': {'displayFields': 42}},
+                          headers=self.headers, status=400)
diff --git a/tests/test_views_schema_group.py b/tests/test_views_schema_group.py
--- a/tests/test_views_schema_group.py
+++ b/tests/test_views_schema_group.py
@@ -79,3 +79,24 @@ def test_groups_are_invalid_if_do_not_match_schema(self):
                           {'data': {'phone': 42}},
                           headers=self.headers,
                           status=400)
+
+
+SCHEMA_UNRESOLVABLE = {
+    'properties': {
+        'phone': {'$ref': '#/definitions/phone'}
+    },
+}
+
+
+class CollectionUnresolvableTest(BaseWebTestWithSchema, unittest.TestCase):
+    def setUp(self):
+        super().setUp()
+        resp = self.app.put_json(BUCKET_URL,
+                                 {'data': {'group:schema': SCHEMA_UNRESOLVABLE}},
+                                 headers=self.headers)
+        self.collection = resp.json['data']
+
+    def test_unresolvable_errors_handled(self):
+        self.app.put_json(GROUP_URL,
+                          {'data': {'phone': 42}},
+                          headers=self.headers, status=400)
diff --git a/tests/test_views_schema_record.py b/tests/test_views_schema_record.py
--- a/tests/test_views_schema_record.py
+++ b/tests/test_views_schema_record.py
@@ -368,3 +368,39 @@ def test_records_are_invalid_if_do_not_match_bucket_schema(self):
                            {'data': {'filters': 42, **VALID_RECORD}},
                            headers=self.headers,
                            status=400)
+
+
+SCHEMA_UNRESOLVABLE = {
+    'properties': {
+        'title': {'$ref': '#/definitions/title'}
+    },
+}
+
+
+class RecordsUnresolvableTest(BaseWebTestWithSchema, unittest.TestCase):
+    def setUp(self):
+        super().setUp()
+        resp = self.app.put_json(COLLECTION_URL,
+                                 {'data': {'schema': SCHEMA_UNRESOLVABLE}},
+                                 headers=self.headers)
+        self.collection = resp.json['data']
+
+    def test_unresolvable_errors_handled(self):
+        self.app.post_json(RECORDS_URL,
+                           {'data': {'title': 'b'}},
+                           headers=self.headers,
+                           status=400)
+
+
+class BucketUnresolvableRecordSchema(BaseWebTestWithSchema, unittest.TestCase):
+    def setUp(self):
+        super().setUp()
+        self.app.put_json(BUCKET_URL,
+                          {'data': {'record:schema': SCHEMA_UNRESOLVABLE}},
+                          headers=self.headers)
+
+    def test_records_are_valid_if_match_schema(self):
+        self.app.post_json(RECORDS_URL,
+                           {'data': {'title': 'b'}},
+                           headers=self.headers,
+                           status=400)