Repository: nautobot/nautobot

Issue:
JSON Schema "format" property not enforced
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.9
* Nautobot version: 1.1.0
<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Create & save the following config context schema
```json
{
    "additionalProperties": false,
    "properties": {
        "ntp-servers": {
            "items": {
                "format": "ipv4",
                "type": "string"
            },
            "type": "array"
        }
    },
    "type": "object"
}
```
2. Create the following config context, assign the previous schema:
```json
{"ntp-servers": ["hi"]}
```
3. Click save

<!-- What did you expect to happen? -->
### Expected Behavior
As per the documentation https://nautobot.readthedocs.io/en/stable/additional-features/config-contexts/#config-context-schemas, the specified `format` should be validated and the save refused

<!-- What happened instead? -->
### Observed Behavior
Save is accepted

### Additional informations
https://python-jsonschema.readthedocs.io/en/stable/validate/#validating-formats indicates that by default there's no `format` validation, you can use : 
```py
jsonschema.Draft7Validator(config_context_schema.data_schema, format_checker=jsonschema.draft7_format_checker)
```



Discussion:
Thanks @u1735067 for the report! Apparently we had this one line in there and it dropped out somehow. 

Resolving patch:
diff --git a/nautobot/extras/models/models.py b/nautobot/extras/models/models.py
--- a/nautobot/extras/models/models.py
+++ b/nautobot/extras/models/models.py
@@ -18,6 +18,7 @@
 from graphql import get_default_backend
 from graphql.error import GraphQLSyntaxError
 from graphql.language.ast import OperationDefinition
+from jsonschema import draft7_format_checker
 from jsonschema.exceptions import SchemaError, ValidationError as JSONSchemaValidationError
 from jsonschema.validators import Draft7Validator
 from rest_framework.utils.encoders import JSONEncoder
@@ -457,7 +458,7 @@ def _validate_with_schema(self, data_field, schema_field):
         # If schema is None, then no schema has been specified on the instance and thus no validation should occur.
         if schema:
             try:
-                Draft7Validator(schema.data_schema).validate(data)
+                Draft7Validator(schema.data_schema, format_checker=draft7_format_checker).validate(data)
             except JSONSchemaValidationError as e:
                 raise ValidationError({data_field: [f"Validation using the JSON Schema {schema} failed.", e.message]})
 


Test patch:
diff --git a/nautobot/extras/tests/test_models.py b/nautobot/extras/tests/test_models.py
--- a/nautobot/extras/tests/test_models.py
+++ b/nautobot/extras/tests/test_models.py
@@ -482,7 +482,7 @@ class ConfigContextSchemaTestCase(TestCase):
     """
 
     def setUp(self):
-        context_data = {"a": 123, "b": 456, "c": 777}
+        context_data = {"a": 123, "b": "456", "c": "10.7.7.7"}
 
         # Schemas
         self.schema_validation_pass = ConfigContextSchema.objects.create(
@@ -491,13 +491,33 @@ def setUp(self):
             data_schema={
                 "type": "object",
                 "additionalProperties": False,
-                "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "c": {"type": "integer"}},
+                "properties": {
+                    "a": {"type": "integer"},
+                    "b": {"type": "string"},
+                    "c": {"type": "string", "format": "ipv4"},
+                },
             },
         )
-        self.schema_validation_fail = ConfigContextSchema.objects.create(
-            name="schema-fail",
-            slug="schema-fail",
-            data_schema={"type": "object", "additionalProperties": False, "properties": {"foo": {"type": "string"}}},
+        self.schemas_validation_fail = (
+            ConfigContextSchema.objects.create(
+                name="schema fail (wrong properties)",
+                slug="schema-fail-wrong-properties",
+                data_schema={
+                    "type": "object",
+                    "additionalProperties": False,
+                    "properties": {"foo": {"type": "string"}},
+                },
+            ),
+            ConfigContextSchema.objects.create(
+                name="schema fail (wrong type)",
+                slug="schema-fail-wrong-type",
+                data_schema={"type": "object", "properties": {"b": {"type": "integer"}}},
+            ),
+            ConfigContextSchema.objects.create(
+                name="schema fail (wrong format)",
+                slug="schema-fail-wrong-format",
+                data_schema={"type": "object", "properties": {"b": {"type": "string", "format": "ipv4"}}},
+            ),
         )
 
         # ConfigContext
@@ -546,10 +566,11 @@ def test_existing_config_context_invalid_schema_applied(self):
         And the config context context data is NOT valid for the schema
         Assert calling clean on the config context object DOES raise a ValidationError
         """
-        self.config_context.schema = self.schema_validation_fail
+        for schema in self.schemas_validation_fail:
+            self.config_context.schema = schema
 
-        with self.assertRaises(ValidationError):
-            self.config_context.full_clean()
+            with self.assertRaises(ValidationError):
+                self.config_context.full_clean()
 
     def test_existing_config_context_with_no_schema_applied(self):
         """
@@ -583,10 +604,11 @@ def test_existing_device_invalid_schema_applied(self):
         And the device local_context_data is NOT valid for the schema
         Assert calling clean on the device object DOES raise a ValidationError
         """
-        self.device.local_context_schema = self.schema_validation_fail
+        for schema in self.schemas_validation_fail:
+            self.device.local_context_schema = schema
 
-        with self.assertRaises(ValidationError):
-            self.device.full_clean()
+            with self.assertRaises(ValidationError):
+                self.device.full_clean()
 
     def test_existing_device_with_no_schema_applied(self):
         """
@@ -620,10 +642,11 @@ def test_existing_virtual_machine_invalid_schema_applied(self):
         And the virtual machine local_context_data is NOT valid for the schema
         Assert calling clean on the virtual machine object DOES raise a ValidationError
         """
-        self.virtual_machine.local_context_schema = self.schema_validation_fail
+        for schema in self.schemas_validation_fail:
+            self.virtual_machine.local_context_schema = schema
 
-        with self.assertRaises(ValidationError):
-            self.virtual_machine.full_clean()
+            with self.assertRaises(ValidationError):
+                self.virtual_machine.full_clean()
 
     def test_existing_virtual_machine_with_no_schema_applied(self):
         """