diff --git a/Makefile b/Makefile index 631c271b..fb2c7b86 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,6 @@ help: @echo "make clean Delete development artefacts (cached files, " @echo " dependencies, etc)" @echo "make requirements Compile all requirements files" - @echo "make allow-list Create an SQL file for adding sites to the allow list" .PHONY: dev dev: python @@ -109,8 +108,4 @@ web: python python: @./bin/install-python -.PHONY: allow-list -allow-list: - @tox -qe dev --run-command 'python bin/add_to_allow_list.py' - DOCKER_TAG = dev diff --git a/bin/add_to_allow_list.py b/bin/add_to_allow_list.py index 430b653e..6d64445e 100755 --- a/bin/add_to_allow_list.py +++ b/bin/add_to_allow_list.py @@ -8,23 +8,18 @@ * Read that file (as a CSV) * Spot rows which don't have a result yet - * Check if we can allow them - * Create an SQL file to add to the running server + * Check if we can allow them and add them to the DB if so * Create an updated CSV file with the results of the run """ import csv -import json import os from argparse import ArgumentParser from datetime import date -from pkg_resources import resource_filename -from pyramid.paster import bootstrap +import requests from checkmate.models import Detection, Reason, Source -from checkmate.services import URLCheckerService -from checkmate.url import hash_for_rule parser = ArgumentParser("A script for adding to the allow list") parser.add_argument( @@ -36,7 +31,8 @@ parser.add_argument( "-o", "--output_csv", default="allow_list.done.csv", help="Output CSV file" ) -parser.add_argument("-s", "--sql", default="allow_list.sql", help="Output SQL file") +parser.add_argument("-s", "--session", required=True, help="Admin session cookie value") +parser.add_argument("-r", "--route", required=True, help="Add rule end-point") class AllowListCSV: @@ -94,57 +90,39 @@ def write(cls, handle, rows): ALLOW_LIST_DETECTION = Detection(Reason.NOT_ALLOWED, Source.ALLOW_LIST) -def check_rows(rows, checker): - """Check each row for detections and hash if none are found. +class Checkmate: + def __init__(self, route, session): + self.route = route + self.session = session - This will skip existing rows with results from previous runs. - """ + def allow_url(self, url): + response = requests.post( + self.route, + headers={"Cookie": f"session={self.session}"}, + json={"data": {"type": "AllowRule", "meta": {"url": url}}}, + ) - for row in rows: - # This has already been dealt with - if row.result: - continue + if response.ok: + attributes = response.json()["data"]["attributes"] + hex_hash = attributes["hash"] + rule = attributes["rule"] - # Don't fail fast, so we get all of the detections - reasons = list(checker.check_url(row.approved_url, fail_fast=False)) + return True, f"Allowed as {rule} with hash {hex_hash}" - try: - # We expect a detection from not being on the allow list, so we'll - # remove it, which will trigger a ValueError if it wasn't there - reasons.remove(ALLOW_LIST_DETECTION) - except ValueError: - row.result = "Already allowed" - continue + if response.status_code == 409: + return False, response.json()["errors"][0]["detail"] - # After the expected allow list detection is gone, any remaining - # reasons are because the URL is blocked - if reasons: - row.result = f"Detections found: {reasons}" - else: - rule, hex_hash = hash_for_rule(row.approved_url) - row.result = f"Added to allow list as: '{rule}'" + if response.status_code == 404: + # If we ever sort out the permissiosns / principals stuff we'll get + # a nice 404 / 401 to be able to tell the difference + raise ConnectionError( + "Either your session has expired, or the route you have " + "provided is not correct" + ) - yield rule, hex_hash - - -def create_sql(handle, rule_hashes, tags): - """Write out the hashes into an SQL file for importing into Postgres.""" - - handle.write("INSERT INTO allow_rule (rule, hash, tags)\nVALUES\n") - - tags = json.dumps(list(tags)).strip("[]") - tags = f"{{{tags}}}" - - first = True - for rule, hex_hash in rule_hashes: - if first: - first = False - else: - handle.write(",\n") - - handle.write(f"\t('{rule}', '{hex_hash}', '{tags}')") - - handle.write(";\n") + raise ConnectionError( + f"Unexpected error when connecting to checkmate: {response}: {response.content}" + ) def main(): @@ -153,28 +131,33 @@ def main(): if not os.path.isfile(args.input_csv): raise EnvironmentError(f"Could not find expected file '{args.input_csv}'") - # Check all the rows - + checkmate = Checkmate(route=args.route, session=args.session) rows = list(AllowListCSV.read(args.input_csv)) - config_file = resource_filename("checkmate", "../conf/development.ini") - with bootstrap(config_file) as env: - request = env["request"] - checker = request.find_service(URLCheckerService) + changed = 0 + + for row in rows: + # This has already been dealt with + if row.result: + continue - with request.tm: - rule_hashes = list(check_rows(rows, checker)) + changed += 1 + rule_accepted, row.result = checkmate.allow_url(row.approved_url) - # Create the output files + if rule_accepted: + print(f"Added row: {row}") + else: + print(f"Failed on row: {row}") - with open(args.sql, "w") as handle: - create_sql(handle, rule_hashes=rule_hashes, tags=["manual"]) + if not changed: + print("No rows were altered. No CSV created") + return + # Create the output CSV file with open(args.output_csv, "w") as handle: AllowListCSV.write(handle, rows=rows) - print(f"Created SQL file: {args.sql}") - print(f"Creating CSV file: {args.output_csv}") + print(f"Created CSV file: {args.output_csv}") if __name__ == "__main__": diff --git a/checkmate/exceptions.py b/checkmate/exceptions.py index 129cac84..caff3ddd 100644 --- a/checkmate/exceptions.py +++ b/checkmate/exceptions.py @@ -40,6 +40,18 @@ def serialise(self): return data +class ResourceConflict(JSONAPIException): + """The request cannot be completed as it conflicts with existing state.""" + + status_code = 409 + + +class MalformedJSONBody(JSONAPIException): + """The JSON body is malformed in some way.""" + + status_code = 400 + + class MalformedURL(Exception): """The URL is malformed in some way.""" diff --git a/checkmate/models/db/allow_rule.py b/checkmate/models/db/allow_rule.py index af0c15e7..f4bda76d 100644 --- a/checkmate/models/db/allow_rule.py +++ b/checkmate/models/db/allow_rule.py @@ -4,10 +4,10 @@ from sqlalchemy.dialects.postgresql import ARRAY from checkmate.db import BASE -from checkmate.models.db.mixins import HashMatchMixin +from checkmate.models.db.mixins import HashMatchMixin, JSONAPIMixin -class AllowRule(BASE, HashMatchMixin): +class AllowRule(BASE, HashMatchMixin, JSONAPIMixin): """Rule about allowing a particular resource.""" BULK_UPSERT_INDEX_ELEMENTS = ["rule"] diff --git a/checkmate/models/db/mixins.py b/checkmate/models/db/mixins.py index 93bf0e74..849d3422 100644 --- a/checkmate/models/db/mixins.py +++ b/checkmate/models/db/mixins.py @@ -117,3 +117,25 @@ def _bulk_upsert(cls, session, values, index_elements, update_elements): # never commit the transaction we are working on and it will get rolled # back mark_changed(session) + + +class JSONAPIMixin: + """A mixin for models to add JSON:API related functions.""" + + def to_json_api(self): + """Create a JSON:API resource dict for this object.""" + + if not self.id: + raise ValueError( + "An ID is mandatory to serialise an object in JSON:API. Have you flushed the DB?" + ) + + return { + "type": self.__class__.__name__, + "id": self.id, + "attributes": { + key: getattr(self, key) + for key in self.__table__.columns.keys() + if key != "id" + }, + } diff --git a/checkmate/resource/schema/AllowRule.json b/checkmate/resource/schema/AllowRule.json new file mode 100644 index 00000000..1751ad4d --- /dev/null +++ b/checkmate/resource/schema/AllowRule.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "AllowRule", + + "type": "object", + "additionalProperties": false, + "required": ["data"], + + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": ["type", "meta"], + + "properties": { + "type": {"enum": ["AllowRule"]}, + + "meta": { + "type": "object", + "additionalProperties": false, + "required": ["url"], + + "properties": { + "url": {"type": "string", "format": "public-url"} + } + } + } + } + } +} \ No newline at end of file diff --git a/checkmate/routes.py b/checkmate/routes.py index f852a7e2..01e44c8a 100644 --- a/checkmate/routes.py +++ b/checkmate/routes.py @@ -20,6 +20,8 @@ def add_routes(config): config.add_route("login_callback", "/ui/api/login_callback") config.add_route("logout", "/ui/api/logout") + config.add_route("add_to_allow_list", "/ui/api/rule", request_method="POST") + def includeme(config): # pragma: no cover """Pyramid config.""" diff --git a/checkmate/templates/admin/pages.html.jinja2 b/checkmate/templates/admin/pages.html.jinja2 index ed43dd1f..c854cb99 100644 --- a/checkmate/templates/admin/pages.html.jinja2 +++ b/checkmate/templates/admin/pages.html.jinja2 @@ -1,3 +1,9 @@ + +

Hello {{ request.session.user.name }}

{% set user = request.session.user %} @@ -13,5 +19,10 @@

Session

{{ request.session }} +

Add to allow list

+ + +tox -qe dev --run-command "python bin/add_to_allow_list.py --session={{ session }} --route={{ request.route_url('add_to_allow_list') }}" +
Logout \ No newline at end of file diff --git a/checkmate/validation.py b/checkmate/validation.py new file mode 100644 index 00000000..aac04028 --- /dev/null +++ b/checkmate/validation.py @@ -0,0 +1,66 @@ +"""Validation tools for working with jsonschema.""" + +import json + +from jsonschema import Draft7Validator, FormatChecker, ValidationError +from pkg_resources import resource_stream + +from checkmate.exceptions import MalformedJSONBody +from checkmate.url import CanonicalURL, Domain + +_FORMAT_CHECKER = FormatChecker() + + +@_FORMAT_CHECKER.checks("public-url", raises=(ValueError,)) +def _check_public_url(instance): + """A validator which checks that a given URL is publically available. + + This only uses static data, not an actual check online. + """ + _, netloc, _, _, _, _ = CanonicalURL.canonical_split(instance) + domain = Domain(netloc) + if not domain.is_valid: + raise ValueError("The URL does not have a valid domain") + + if not domain.is_public: + raise ValueError("The URL is not public") + + return True + + +def get_validator(schema_path): + """Get a jsonschema validator object for a given schema path. + + :param schema_path: Path relative to the checkmate root + """ + + return Draft7Validator( + json.load(resource_stream("checkmate", schema_path)), + format_checker=_FORMAT_CHECKER, + ) + + +def get_validated_json_body(request, validator): + """Get the JSON body of a request validated against a jsonschema + + :param request: Pyramid request object + :param validator: A jsonschema validator (see `get_validator()`) + :return: The json dict if validation is successful + + :raise MalformedJSONBody: If the JSON cannot be decoded or the body + does not conform to the schema provided + """ + + try: + body = request.json_body + except ValueError as err: + raise MalformedJSONBody(f"Posted JSON missing or malformed: {err}") from err + + try: + validator.validate(body) + except ValidationError as err: + raise MalformedJSONBody( + f"JSON body does not match expected schema: {err}" + ) from err + + return body diff --git a/checkmate/views/ui/admin.py b/checkmate/views/ui/admin.py index c43d7654..9f0ebb99 100644 --- a/checkmate/views/ui/admin.py +++ b/checkmate/views/ui/admin.py @@ -1,4 +1,6 @@ """User feedback for blocked pages.""" +from http.cookies import SimpleCookie + from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config @@ -10,10 +12,13 @@ renderer="checkmate:templates/admin/pages.html.jinja2", effective_principals=[Principals.STAFF], ) -def admin_pages(_context, _request): +def admin_pages(_context, request): """Render an HTML version of a blocked URL with explanation.""" - return {} + cookie = SimpleCookie() + cookie.load(request.headers["Cookie"]) + + return {"session": cookie["session"].value} @view_config(route_name="admin_pages") diff --git a/checkmate/views/ui/api/add_to_allow_list.py b/checkmate/views/ui/api/add_to_allow_list.py new file mode 100644 index 00000000..6f34ed39 --- /dev/null +++ b/checkmate/views/ui/api/add_to_allow_list.py @@ -0,0 +1,53 @@ +"""User feedback for blocked pages.""" + +from pyramid.view import view_config + +from checkmate.exceptions import ResourceConflict +from checkmate.models import AllowRule, Detection, Principals, Reason, Source +from checkmate.services import URLCheckerService +from checkmate.url import hash_for_rule +from checkmate.validation import get_validated_json_body, get_validator + +_ALLOW_LIST_DETECTION = Detection(Reason.NOT_ALLOWED, Source.ALLOW_LIST) +_ALLOW_RULE_VALIDATOR = get_validator("resource/schema/AllowRule.json") + + +@view_config( + route_name="add_to_allow_list", + request_method="POST", + renderer="json", + effective_principals=[Principals.STAFF], +) +def add_to_allow_list(_context, request): + """Render an HTML version of a blocked URL with explanation.""" + + body = get_validated_json_body(request, _ALLOW_RULE_VALIDATOR) + url = body["data"]["meta"]["url"] + + # Check this isn't something really dumb like 'co.uk' which will ruin the + # allow list + + # Don't fail fast, so we get all of the detections + checker = request.find_service(URLCheckerService) + reasons = list(checker.check_url(url, fail_fast=False)) + + try: + # We expect a detection from not being on the allow list, so we'll + # remove it, which will trigger a ValueError if it wasn't there + reasons.remove(_ALLOW_LIST_DETECTION) + except ValueError: + raise ResourceConflict("Requested URL is already allowed") from None + + # After the expected allow list detection is gone, any remaining + # reasons are because the URL is blocked + if reasons: + raise ResourceConflict(f"Cannot allow URL as reasons to block found: {reasons}") + + rule_string, hex_hash = hash_for_rule(url) + + rule = AllowRule(rule=rule_string, hash=hex_hash, tags=["manual"]) + request.db.add(rule) + request.db.flush() # Make sure an id is allocated before we serialise + + # https://jsonapi.org/format/#document-top-level + return {"data": rule.to_json_api()} diff --git a/requirements/dev.txt b/requirements/dev.txt index f5b5081e..3bb7dcb4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -6,7 +6,7 @@ # alembic==1.4.3 # via -r requirements/requirements.txt amqp==5.0.2 # via -r requirements/requirements.txt, kombu -attrs==20.3.0 # via jsonschema +attrs==20.3.0 # via -r requirements/requirements.txt, jsonschema backcall==0.2.0 # via ipython bcrypt==3.2.0 # via paramiko billiard==3.6.3.0 # via -r requirements/requirements.txt, celery @@ -41,7 +41,7 @@ ipython-genutils==0.2.0 # via traitlets ipython==7.16.1 # via -r requirements/dev.in, ipdb, pyramid-ipython jedi==0.17.2 # via ipython jinja2==2.11.2 # via -r requirements/requirements.txt, pyramid-jinja2 -jsonschema==3.2.0 # via docker-compose +jsonschema==3.2.0 # via -r requirements/requirements.txt, docker-compose jwt==1.1.0 # via -r requirements/requirements.txt kombu==5.0.2 # via -r requirements/requirements.txt, celery mako==1.1.3 # via -r requirements/requirements.txt, alembic @@ -71,7 +71,7 @@ pyramid-sanity==1.0.1 # via -r requirements/requirements.txt pyramid-services==2.2 # via -r requirements/requirements.txt pyramid-tm==2.4 # via -r requirements/requirements.txt pyramid==1.10.5 # via -r requirements/requirements.txt, h-pyramid-sentry, pyramid-exclog, pyramid-ipython, pyramid-jinja2, pyramid-sanity, pyramid-services, pyramid-tm -pyrsistent==0.17.3 # via jsonschema +pyrsistent==0.17.3 # via -r requirements/requirements.txt, jsonschema python-dateutil==2.8.1 # via -r requirements/requirements.txt, alembic, faker python-dotenv==0.15.0 # via docker-compose python-editor==1.0.4 # via -r requirements/requirements.txt, alembic diff --git a/requirements/functests.txt b/requirements/functests.txt index 046a1949..37aabdb6 100644 --- a/requirements/functests.txt +++ b/requirements/functests.txt @@ -6,7 +6,7 @@ # alembic==1.4.3 # via -r requirements/requirements.txt amqp==5.0.2 # via -r requirements/requirements.txt, kombu -attrs==20.3.0 # via pytest +attrs==20.3.0 # via -r requirements/requirements.txt, jsonschema, pytest beautifulsoup4==4.9.3 # via webtest billiard==3.6.3.0 # via -r requirements/requirements.txt, celery cachetools==4.2.1 # via -r requirements/requirements.txt, google-auth @@ -25,10 +25,11 @@ h-matchers==1.2.10 # via -r requirements/functests.in h-pyramid-sentry==1.2.1 # via -r requirements/requirements.txt hupper==1.10.2 # via -r requirements/requirements.txt, pyramid idna==2.10 # via -r requirements/requirements.txt, requests -importlib-metadata==3.1.0 # via -r requirements/requirements.txt, kombu, pluggy, pytest +importlib-metadata==3.1.0 # via -r requirements/requirements.txt, jsonschema, kombu, pluggy, pytest importlib-resources==3.3.0 # via -r requirements/requirements.txt, netaddr iniconfig==1.1.1 # via pytest jinja2==2.11.2 # via -r requirements/requirements.txt, pyramid-jinja2 +jsonschema==3.2.0 # via -r requirements/requirements.txt jwt==1.1.0 # via -r requirements/requirements.txt kombu==5.0.2 # via -r requirements/requirements.txt, celery mako==1.1.3 # via -r requirements/requirements.txt, alembic @@ -54,6 +55,7 @@ pyramid-sanity==1.0.1 # via -r requirements/requirements.txt pyramid-services==2.2 # via -r requirements/requirements.txt pyramid-tm==2.4 # via -r requirements/requirements.txt pyramid==1.10.5 # via -r requirements/requirements.txt, h-pyramid-sentry, pyramid-exclog, pyramid-jinja2, pyramid-sanity, pyramid-services, pyramid-tm +pyrsistent==0.17.3 # via -r requirements/requirements.txt, jsonschema pytest==6.2.1 # via -r requirements/functests.in python-dateutil==2.8.1 # via -r requirements/requirements.txt, alembic python-editor==1.0.4 # via -r requirements/requirements.txt, alembic @@ -62,7 +64,7 @@ requests-oauthlib==1.3.0 # via -r requirements/requirements.txt, google-auth-oa requests==2.25.0 # via -r requirements/requirements.txt, requests-oauthlib rsa==4.7 # via -r requirements/requirements.txt, google-auth sentry-sdk==0.19.4 # via -r requirements/requirements.txt, h-pyramid-sentry -six==1.15.0 # via -r requirements/requirements.txt, click-repl, cryptography, google-auth, python-dateutil, webtest +six==1.15.0 # via -r requirements/requirements.txt, click-repl, cryptography, google-auth, jsonschema, python-dateutil, webtest soupsieve==2.1 # via beautifulsoup4 sqlalchemy==1.3.20 # via -r requirements/requirements.txt, alembic, zope.sqlalchemy toml==0.10.2 # via pytest diff --git a/requirements/lint.txt b/requirements/lint.txt index af1db3fb..361ca86e 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -7,7 +7,7 @@ alembic==1.4.3 # via -r requirements/requirements.txt, -r requirements/tests.txt amqp==5.0.2 # via -r requirements/requirements.txt, -r requirements/tests.txt, kombu astroid==2.4.2 # via pylint -attrs==20.3.0 # via -r requirements/tests.txt, pytest +attrs==20.3.0 # via -r requirements/requirements.txt, -r requirements/tests.txt, jsonschema, pytest beautifulsoup4==4.9.3 # via -r requirements/tests.txt, webtest billiard==3.6.3.0 # via -r requirements/requirements.txt, -r requirements/tests.txt, celery cachetools==4.2.1 # via -r requirements/requirements.txt, -r requirements/tests.txt, google-auth @@ -31,11 +31,12 @@ htmlmin==0.1.12 # via -r requirements/lint.in httpretty==1.0.3 # via -r requirements/tests.txt hupper==1.10.2 # via -r requirements/requirements.txt, -r requirements/tests.txt, pyramid idna==2.10 # via -r requirements/requirements.txt, -r requirements/tests.txt, requests -importlib-metadata==3.1.0 # via -r requirements/requirements.txt, -r requirements/tests.txt, kombu, pluggy, pytest +importlib-metadata==3.1.0 # via -r requirements/requirements.txt, -r requirements/tests.txt, jsonschema, kombu, pluggy, pytest importlib-resources==3.3.0 # via -r requirements/requirements.txt, -r requirements/tests.txt, netaddr iniconfig==1.1.1 # via -r requirements/tests.txt, pytest isort==5.6.4 # via pylint jinja2==2.11.2 # via -r requirements/requirements.txt, -r requirements/tests.txt, pyramid-jinja2 +jsonschema==3.2.0 # via -r requirements/requirements.txt, -r requirements/tests.txt jwt==1.1.0 # via -r requirements/requirements.txt, -r requirements/tests.txt kombu==5.0.2 # via -r requirements/requirements.txt, -r requirements/tests.txt, celery lazy-object-proxy==1.4.3 # via astroid @@ -65,6 +66,7 @@ pyramid-sanity==1.0.1 # via -r requirements/requirements.txt, -r requirement pyramid-services==2.2 # via -r requirements/requirements.txt, -r requirements/tests.txt pyramid-tm==2.4 # via -r requirements/requirements.txt, -r requirements/tests.txt pyramid==1.10.5 # via -r requirements/requirements.txt, -r requirements/tests.txt, h-pyramid-sentry, pyramid-exclog, pyramid-jinja2, pyramid-sanity, pyramid-services, pyramid-tm +pyrsistent==0.17.3 # via -r requirements/requirements.txt, -r requirements/tests.txt, jsonschema pytest==6.1.2 # via -r requirements/tests.txt python-dateutil==2.8.1 # via -r requirements/requirements.txt, -r requirements/tests.txt, alembic, faker python-editor==1.0.4 # via -r requirements/requirements.txt, -r requirements/tests.txt, alembic @@ -75,7 +77,7 @@ requests==2.25.0 # via -r requirements/requirements.txt, -r requirement rjsmin==1.1.0 # via -r requirements/lint.in rsa==4.7 # via -r requirements/requirements.txt, -r requirements/tests.txt, google-auth sentry-sdk==0.19.4 # via -r requirements/requirements.txt, -r requirements/tests.txt, h-pyramid-sentry -six==1.15.0 # via -r requirements/requirements.txt, -r requirements/tests.txt, astroid, click-repl, cryptography, google-auth, packaging, python-dateutil, webtest +six==1.15.0 # via -r requirements/requirements.txt, -r requirements/tests.txt, astroid, click-repl, cryptography, google-auth, jsonschema, packaging, python-dateutil, webtest snowballstemmer==2.0.0 # via pydocstyle soupsieve==2.0.1 # via -r requirements/tests.txt, beautifulsoup4 sqlalchemy==1.3.20 # via -r requirements/requirements.txt, -r requirements/tests.txt, alembic, zope.sqlalchemy diff --git a/requirements/requirements.in b/requirements/requirements.in index 65937348..828c7b12 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -2,6 +2,7 @@ alembic celery google-auth-oauthlib gunicorn +jsonschema jwt psycopg2 pyramid diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 472f235f..5e369300 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,6 +6,7 @@ # alembic==1.4.3 # via -r requirements/requirements.in amqp==5.0.2 # via kombu +attrs==20.3.0 # via jsonschema billiard==3.6.3.0 # via celery cachetools==4.2.1 # via google-auth celery==5.0.2 # via -r requirements/requirements.in @@ -22,9 +23,10 @@ gunicorn==20.0.4 # via -r requirements/requirements.in h-pyramid-sentry==1.2.1 # via -r requirements/requirements.in hupper==1.10.2 # via pyramid idna==2.10 # via requests -importlib-metadata==3.1.0 # via kombu +importlib-metadata==3.1.0 # via jsonschema, kombu importlib-resources==3.3.0 # via netaddr jinja2==2.11.2 # via pyramid-jinja2 +jsonschema==3.2.0 # via -r requirements/requirements.in jwt==1.1.0 # via -r requirements/requirements.in kombu==5.0.2 # via celery mako==1.1.3 # via alembic @@ -46,6 +48,7 @@ pyramid-sanity==1.0.1 # via -r requirements/requirements.in pyramid-services==2.2 # via -r requirements/requirements.in pyramid-tm==2.4 # via -r requirements/requirements.in pyramid==1.10.5 # via -r requirements/requirements.in, h-pyramid-sentry, pyramid-exclog, pyramid-jinja2, pyramid-sanity, pyramid-services, pyramid-tm +pyrsistent==0.17.3 # via jsonschema python-dateutil==2.8.1 # via alembic python-editor==1.0.4 # via alembic pytz==2020.4 # via celery @@ -53,7 +56,7 @@ requests-oauthlib==1.3.0 # via google-auth-oauthlib requests==2.25.0 # via -r requirements/requirements.in, requests-oauthlib rsa==4.7 # via google-auth sentry-sdk==0.19.4 # via h-pyramid-sentry -six==1.15.0 # via click-repl, cryptography, google-auth, python-dateutil +six==1.15.0 # via click-repl, cryptography, google-auth, jsonschema, python-dateutil sqlalchemy==1.3.20 # via -r requirements/requirements.in, alembic, zope.sqlalchemy transaction==3.0.0 # via pyramid-tm, zope.sqlalchemy translationstring==1.4 # via pyramid diff --git a/requirements/tests.txt b/requirements/tests.txt index 45b0d144..e84b514b 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -6,7 +6,7 @@ # alembic==1.4.3 # via -r requirements/requirements.txt amqp==5.0.2 # via -r requirements/requirements.txt, kombu -attrs==20.3.0 # via pytest +attrs==20.3.0 # via -r requirements/requirements.txt, jsonschema, pytest beautifulsoup4==4.9.3 # via webtest billiard==3.6.3.0 # via -r requirements/requirements.txt, celery cachetools==4.2.1 # via -r requirements/requirements.txt, google-auth @@ -29,10 +29,11 @@ h-pyramid-sentry==1.2.1 # via -r requirements/requirements.txt httpretty==1.0.3 # via -r requirements/tests.in hupper==1.10.2 # via -r requirements/requirements.txt, pyramid idna==2.10 # via -r requirements/requirements.txt, requests -importlib-metadata==3.1.0 # via -r requirements/requirements.txt, kombu, pluggy, pytest +importlib-metadata==3.1.0 # via -r requirements/requirements.txt, jsonschema, kombu, pluggy, pytest importlib-resources==3.3.0 # via -r requirements/requirements.txt, netaddr iniconfig==1.1.1 # via pytest jinja2==2.11.2 # via -r requirements/requirements.txt, pyramid-jinja2 +jsonschema==3.2.0 # via -r requirements/requirements.txt jwt==1.1.0 # via -r requirements/requirements.txt kombu==5.0.2 # via -r requirements/requirements.txt, celery mako==1.1.3 # via -r requirements/requirements.txt, alembic @@ -58,6 +59,7 @@ pyramid-sanity==1.0.1 # via -r requirements/requirements.txt pyramid-services==2.2 # via -r requirements/requirements.txt pyramid-tm==2.4 # via -r requirements/requirements.txt pyramid==1.10.5 # via -r requirements/requirements.txt, h-pyramid-sentry, pyramid-exclog, pyramid-jinja2, pyramid-sanity, pyramid-services, pyramid-tm +pyrsistent==0.17.3 # via -r requirements/requirements.txt, jsonschema pytest==6.1.2 # via -r requirements/tests.in python-dateutil==2.8.1 # via -r requirements/requirements.txt, alembic, faker python-editor==1.0.4 # via -r requirements/requirements.txt, alembic @@ -66,7 +68,7 @@ requests-oauthlib==1.3.0 # via -r requirements/requirements.txt, google-auth-oa requests==2.25.0 # via -r requirements/requirements.txt, requests-oauthlib rsa==4.7 # via -r requirements/requirements.txt, google-auth sentry-sdk==0.19.4 # via -r requirements/requirements.txt, h-pyramid-sentry -six==1.15.0 # via -r requirements/requirements.txt, click-repl, cryptography, google-auth, packaging, python-dateutil, webtest +six==1.15.0 # via -r requirements/requirements.txt, click-repl, cryptography, google-auth, jsonschema, packaging, python-dateutil, webtest soupsieve==2.0.1 # via beautifulsoup4 sqlalchemy==1.3.20 # via -r requirements/requirements.txt, alembic, zope.sqlalchemy text-unidecode==1.3 # via faker