From ecc21ebe34c738eba7ff0a30cf0bab17d8ab81d6 Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Thu, 4 Jan 2024 21:09:07 +0000 Subject: [PATCH 1/9] feat: Add codejail_service app for transition to containerized codejail --- CHANGELOG.rst | 6 + edx_arch_experiments/__init__.py | 2 +- .../codejail_service/README.rst | 27 ++++ .../codejail_service/__init__.py | 0 edx_arch_experiments/codejail_service/apps.py | 21 +++ edx_arch_experiments/codejail_service/urls.py | 11 ++ .../codejail_service/views.py | 153 ++++++++++++++++++ requirements/base.in | 5 + requirements/base.txt | 110 ++++++++++++- requirements/dev.txt | 87 +++++++++- requirements/doc.txt | 124 ++++++++++++-- requirements/pip-tools.txt | 2 +- requirements/pip.txt | 4 +- requirements/quality.txt | 112 +++++++++++-- requirements/scripts.txt | 137 +++++++++++++--- requirements/test.txt | 140 ++++++++++++++-- setup.py | 1 + 17 files changed, 877 insertions(+), 65 deletions(-) create mode 100644 edx_arch_experiments/codejail_service/README.rst create mode 100644 edx_arch_experiments/codejail_service/__init__.py create mode 100644 edx_arch_experiments/codejail_service/apps.py create mode 100644 edx_arch_experiments/codejail_service/urls.py create mode 100644 edx_arch_experiments/codejail_service/views.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34e9765..99d1bcb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,12 @@ Change Log Unreleased ~~~~~~~~~~ +[3.2.0] - 2024-01-09 +~~~~~~~~~~~~~~~~~~~~ +Added +_____ +* Add ``codejail_service`` app for transition to containerized codejail + [3.1.1] - 2023-11-06 ~~~~~~~~~~~~~~~~~~~~ Fixed diff --git a/edx_arch_experiments/__init__.py b/edx_arch_experiments/__init__.py index 2bf91e3..0960636 100644 --- a/edx_arch_experiments/__init__.py +++ b/edx_arch_experiments/__init__.py @@ -2,4 +2,4 @@ A plugin to include applications under development by the architecture team at 2U. """ -__version__ = '3.1.1' +__version__ = '3.2.0' diff --git a/edx_arch_experiments/codejail_service/README.rst b/edx_arch_experiments/codejail_service/README.rst new file mode 100644 index 0000000..ec97d4b --- /dev/null +++ b/edx_arch_experiments/codejail_service/README.rst @@ -0,0 +1,27 @@ +Codejail Service +################ + +When installed in the LMS as a plugin app, the ``codejail_service`` app allows the CMS to delegate codejail executions to the LMS across the network. + +This is intended as a `temporary situation `_ with the following goals: + +- Unblock containerization of the CMS. Codejail cannot be readily containerized due to its reliance on AppArmor, but if codejail execution is outsourced, then we can containerize CMS first and will be in a better position to containerize the LMS afterwards. +- Exercise the remote-codejail pathway and have an opportunity to discover and implement needed improvements before fully building out a separate, dedicated codejail service. + +Usage +***** + +In LMS: + +- Install ``edx-arch-experiments`` as a dependency +- Identify a service account that will be permitted to make calls to the codejail service and ensure it has the ``is_staff`` Django flag. In devstack, this would be ``cms_worker``. +- In Djano admin, under ``Django OAuth Toolkit > Applications``, find or create a Client Credentials application for that service user. + +In CMS: + +- Set ``ENABLE_CODEJAIL_REST_SERVICE`` to ``True`` +- Set ``CODE_JAIL_REST_SERVICE_HOST`` to the URL origin of the LMS (e.g. ``http://edx.devstack.lms:18000`` in devstack) +- Keep ``CODE_JAIL_REST_SERVICE_REMOTE_EXEC`` at its default of ``xmodule.capa.safe_exec.remote_exec.send_safe_exec_request_v0`` +- Adjust ``CODE_JAIL_REST_SERVICE_CONNECT_TIMEOUT`` and ``CODE_JAIL_REST_SERVICE_READ_TIMEOUT`` if needed +- Set ``CODE_JAIL_REST_SERVICE_OAUTH_URL`` to the LMS OAuth endpoint (e.g. ``http://edx.devstack.lms:18000`` in devstack) +- Set ``CODE_JAIL_REST_SERVICE_OAUTH_CLIENT_ID`` and ``CODE_JAIL_REST_SERVICE_OAUTH_CLIENT_SECRET`` to the client credentials app ID and secret that you identified in the LMS. (In devstack, these would be ``cms-backend-service-key`` and ``cms-backend-service-secret``.) diff --git a/edx_arch_experiments/codejail_service/__init__.py b/edx_arch_experiments/codejail_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/edx_arch_experiments/codejail_service/apps.py b/edx_arch_experiments/codejail_service/apps.py new file mode 100644 index 0000000..6cdace6 --- /dev/null +++ b/edx_arch_experiments/codejail_service/apps.py @@ -0,0 +1,21 @@ +""" +App for running answer submissions inside of codejail. +""" + +from django.apps import AppConfig +from edx_django_utils.plugins.constants import PluginURLs + + +class CodejailService(AppConfig): + """ + Django application to run things in codejail. + """ + name = 'edx_arch_experiments.codejail_service' + + plugin_app = { + PluginURLs.CONFIG: { + 'lms.djangoapp': { + PluginURLs.NAMESPACE: 'codejail_service', + } + }, + } diff --git a/edx_arch_experiments/codejail_service/urls.py b/edx_arch_experiments/codejail_service/urls.py new file mode 100644 index 0000000..18e6d32 --- /dev/null +++ b/edx_arch_experiments/codejail_service/urls.py @@ -0,0 +1,11 @@ +""" +Codejail service URLs. +""" + +from django.urls import path + +from . import views + +urlpatterns = [ + path('api/v0/code-exec', views.code_exec_view_v0, name="code_exec_v0"), +] diff --git a/edx_arch_experiments/codejail_service/views.py b/edx_arch_experiments/codejail_service/views.py new file mode 100644 index 0000000..49bba3b --- /dev/null +++ b/edx_arch_experiments/codejail_service/views.py @@ -0,0 +1,153 @@ +""" +Codejail service API. +""" + +import json +import logging +from copy import deepcopy + +import jsonschema +from codejail.safe_exec import SafeExecException, safe_exec +from django.conf import settings +from edx_toggles.toggles import SettingToggle +from rest_framework.decorators import api_view, parser_classes, permission_classes +from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response + +log = logging.getLogger(__name__) + +# .. toggle_name: CODEJAIL_SERVICE_ENABLED +# .. toggle_implementation: SettingToggle +# .. toggle_default: True +# .. toggle_description: If True, codejail execution calls will be accepted over the network, +# allowing this IDA to act as a codejail service for another IDA. +# .. toggle_use_cases: circuit_breaker +# .. toggle_creation_date: 2023-12-21 +# .. toggle_tickets: https://github.com/openedx/edx-platform/issues/33538 +CODEJAIL_SERVICE_ENABLED = SettingToggle('CODEJAIL_SERVICE_ENABLED', default=True, module_name=__name__) + +# Schema for the JSON passed in the v0 API's 'payload' field. +payload_schema = { + 'type': 'object', + 'properties': { + 'code': {'type': 'string'}, + 'globals_dict': {'type': 'object'}, + # Some of these are configured as union types because + # edx-platform appears to currently default to None for some + # of them (rather than omitting the keys.) + 'python_path': { + 'anyOf': [ + { + 'type': 'array', + 'items': {'type': 'string'}, + }, + {'type': 'null'}, + ], + }, + 'limit_overrides_context': { + 'anyOf': [ + {'type': 'string'}, + {'type': 'null'}, + ], + }, + 'slug': { + 'anyOf': [ + {'type': 'string'}, + {'type': 'null'}, + ], + }, + 'unsafely': {'type': 'boolean'}, + }, + 'required': ['code'], +} + +# A note on the authorization model used here: +# +# We really just need one service account to be able to call this, and +# then also allow is_staff to call it for convenience and debugging +# purposes. +# +# To do this "right", I'd probably have to create an empty abstract +# model, create a permission on it, create a group, add the permission +# to the group, and add the service account to the group. Then I could +# check the calling user's has_perm. If I wanted to use bridgekeeper +# (as we're trying to do more of) I might be able to give bridgekeeper +# a `@blanket_rule` that checks membership in the group, then use +# bridgekeeper here instead of checking permissions directly, but it's +# possible this wouldn't work because bridgekeeper might require there +# to be a model instance to pass in (and there wouldn't be one, since +# it's just an abstract model.) +# +# But... given that the service account will be is_staff, and I'll be +# opening this up to is_staff alongside the intended service account, +# and this is already a hacky intermediate solution... we can just use +# the `IsAdminUser` permission class and be done with it. + + +@api_view(['POST']) +@parser_classes([FormParser, MultiPartParser]) +@permission_classes([IsAdminUser]) +def code_exec_view_v0(request): + """ + Executes code in a codejail sandbox for a remote caller. + + This implements the API used by edxapp's xmodule.capa.safe_exec.remote_exec. + It accepts a POST of a form containing a `payload` value and zero or more + extra files. + + The payload is JSON and contains the parameters to be sent to codejail's + safe_exec (aside from `extra_files`). See payload_schema for type information. + + This API does not permit `unsafely=true`. + """ + if not CODEJAIL_SERVICE_ENABLED.is_enabled(): + return Response("Codejail service not enabled", status=500) + + params_json = request.data['payload'] + params = json.loads(params_json) + jsonschema.validate(params, payload_schema) + + complete_code = params['code'] # includes standard prolog + input_globals_dict = params.get('globals_dict') or {} + python_path = params.get('python_path') or [] + limit_overrides_context = params.get('limit_overrides_context') + slug = params.get('slug') + unsafely = params.get('unsafely') + + extra_files = request.FILES + + # There's a risk of getting into a loop if e.g. the CMS asks the + # LMS to run codejail executions on its behalf, and the LMS is + # *also* inadvertently configured to call the LMS (itself). + # There's no good reason to have a chain of >2 services passing + # codejail requests along, so only allow execution here if we + # aren't going to pass it along to someone else. + if getattr(settings, 'ENABLE_CODEJAIL_REST_SERVICE', False): + raise Exception( + "Refusing to run codejail request from over the network " + "when we're going to pass it to another IDA anyway" + ) + + # Far too dangerous to allow unsafe executions to come in over the + # network, no matter who we think the caller is. The caller is the + # one who has the context on safety. + if unsafely: + raise Exception("Refusing to run codejail request from over the network with unsafely=true") + + output_globals_dict = deepcopy(input_globals_dict) # Output dict will be mutated by safe_exec + try: + safe_exec( + complete_code, + output_globals_dict, + python_path=python_path, + extra_files=extra_files, + limit_overrides_context=limit_overrides_context, + slug=slug, + ) + except SafeExecException as e: + log.debug("CodejailService execution failed with: {e!r}") + return Response({'emsg': f"Code jail execution failed: {e!r}"}) + + log.debug("CodejailService execution succeeded, with globals={output_globals_dict!r}") + return Response({'globals_dict': output_globals_dict}) diff --git a/requirements/base.in b/requirements/base.in index 0935bef..924dd37 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,3 +4,8 @@ Django # Web application framework edx_django_utils django-waffle # Configuration switches and flags -- used by config_watcher app +edx-codejail # Actual codejail library; used by codejail_service app +djangorestframework # Used by codejail_service app +edx-drf-extensions # Used by codejail_service app +edx-toggles # Used by codejail_service app +jsonschema # Parse and validate JSON; used by codejail_service app diff --git a/requirements/base.txt b/requirements/base.txt index bb88a7c..49f7249 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,40 +6,136 @@ # asgiref==3.7.2 # via django +attrs==23.2.0 + # via + # jsonschema + # referencing +certifi==2023.11.17 + # via requests cffi==1.16.0 - # via pynacl + # via + # cryptography + # pynacl +charset-normalizer==3.3.2 + # via requests click==8.1.7 - # via edx-django-utils + # via + # code-annotations + # edx-django-utils +code-annotations==1.5.0 + # via edx-toggles +cryptography==41.0.7 + # via pyjwt django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in # django-crum # django-waffle + # djangorestframework + # drf-jwt # edx-django-utils + # edx-drf-extensions + # edx-toggles django-crum==0.7.9 - # via edx-django-utils + # via + # edx-django-utils + # edx-toggles django-waffle==4.1.0 # via # -r requirements/base.in # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/base.in + # drf-jwt + # edx-drf-extensions +drf-jwt==1.19.2 + # via edx-drf-extensions +edx-codejail==3.3.3 + # via -r requirements/base.in edx-django-utils==5.9.0 + # via + # -r requirements/base.in + # edx-drf-extensions + # edx-toggles +edx-drf-extensions==9.0.1 # via -r requirements/base.in +edx-opaque-keys==2.5.1 + # via edx-drf-extensions +edx-toggles==5.1.0 + # via -r requirements/base.in +idna==3.6 + # via requests +importlib-resources==6.1.1 + # via + # jsonschema + # jsonschema-specifications +jinja2==3.1.2 + # via code-annotations +jsonschema==4.20.0 + # via -r requirements/base.in +jsonschema-specifications==2023.12.1 + # via jsonschema +markupsafe==2.1.3 + # via jinja2 newrelic==9.3.0 # via edx-django-utils pbr==6.0.0 # via stevedore -psutil==5.9.6 +pkgutil-resolve-name==1.3.10 + # via jsonschema +psutil==5.9.7 # via edx-django-utils pycparser==2.21 # via cffi +pyjwt[crypto]==2.8.0 + # via + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via edx-opaque-keys pynacl==1.5.0 # via edx-django-utils +python-slugify==8.0.1 + # via code-annotations pytz==2023.3.post1 - # via django + # via + # django + # djangorestframework +pyyaml==6.0.1 + # via code-annotations +referencing==0.32.0 + # via + # jsonschema + # jsonschema-specifications +requests==2.31.0 + # via edx-drf-extensions +rpds-py==0.16.2 + # via + # jsonschema + # referencing +semantic-version==2.10.0 + # via edx-drf-extensions +six==1.16.0 + # via edx-codejail sqlparse==0.4.4 # via django stevedore==5.1.0 - # via edx-django-utils + # via + # code-annotations + # edx-django-utils + # edx-opaque-keys +text-unidecode==1.3 + # via python-slugify typing-extensions==4.9.0 - # via asgiref + # via + # asgiref + # edx-opaque-keys +urllib3==2.1.0 + # via requests +zipp==3.17.0 + # via importlib-resources diff --git a/requirements/dev.txt b/requirements/dev.txt index 6c5aa7e..c6043d6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -13,6 +13,11 @@ astroid==3.0.2 # -r requirements/quality.txt # pylint # pylint-celery +attrs==23.2.0 + # via + # -r requirements/quality.txt + # jsonschema + # referencing build==1.0.3 # via # -r requirements/pip-tools.txt @@ -56,11 +61,12 @@ code-annotations==1.5.0 # via # -r requirements/quality.txt # edx-lint + # edx-toggles colorama==0.4.6 # via # -r requirements/ci.txt # tox -coverage[toml]==7.3.3 +coverage[toml]==7.4.0 # via # -r requirements/quality.txt # coverage @@ -68,6 +74,7 @@ coverage[toml]==7.3.3 cryptography==41.0.7 # via # -r requirements/quality.txt + # pyjwt # secretstorage diff-cover==8.0.2 # via -r requirements/dev.in @@ -85,26 +92,55 @@ django==3.2.23 # -r requirements/quality.txt # django-crum # django-waffle + # djangorestframework + # drf-jwt # edx-django-utils + # edx-drf-extensions # edx-i18n-tools + # edx-toggles django-crum==0.7.9 # via # -r requirements/quality.txt # edx-django-utils + # edx-toggles django-waffle==4.1.0 # via # -r requirements/quality.txt # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/quality.txt + # drf-jwt + # edx-drf-extensions docutils==0.20.1 # via # -r requirements/quality.txt # readme-renderer +drf-jwt==1.19.2 + # via + # -r requirements/quality.txt + # edx-drf-extensions +edx-codejail==3.3.3 + # via -r requirements/quality.txt edx-django-utils==5.9.0 + # via + # -r requirements/quality.txt + # edx-drf-extensions + # edx-toggles +edx-drf-extensions==9.0.1 # via -r requirements/quality.txt edx-i18n-tools==1.3.0 # via -r requirements/dev.in edx-lint==5.3.6 # via -r requirements/quality.txt +edx-opaque-keys==2.5.1 + # via + # -r requirements/quality.txt + # edx-drf-extensions +edx-toggles==5.1.0 + # via -r requirements/quality.txt exceptiongroup==1.2.0 # via # -r requirements/quality.txt @@ -118,7 +154,7 @@ idna==3.6 # via # -r requirements/quality.txt # requests -importlib-metadata==7.0.0 +importlib-metadata==7.0.1 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt @@ -128,6 +164,8 @@ importlib-metadata==7.0.0 importlib-resources==6.1.1 # via # -r requirements/quality.txt + # jsonschema + # jsonschema-specifications # keyring iniconfig==2.0.0 # via @@ -151,11 +189,17 @@ jinja2==3.1.2 # -r requirements/quality.txt # code-annotations # diff-cover +jsonschema==4.20.0 + # via -r requirements/quality.txt +jsonschema-specifications==2023.12.1 + # via + # -r requirements/quality.txt + # jsonschema keyring==24.3.0 # via # -r requirements/quality.txt # twine -lxml==4.9.3 +lxml==5.0.0 # via edx-i18n-tools markdown-it-py==3.0.0 # via @@ -206,6 +250,10 @@ pkginfo==1.9.6 # via # -r requirements/quality.txt # twine +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/quality.txt + # jsonschema platformdirs==4.1.0 # via # -r requirements/ci.txt @@ -222,7 +270,7 @@ pluggy==1.3.0 # tox polib==1.2.0 # via edx-i18n-tools -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/quality.txt # edx-django-utils @@ -240,6 +288,12 @@ pygments==2.17.2 # diff-cover # readme-renderer # rich +pyjwt[crypto]==2.8.0 + # via + # -r requirements/quality.txt + # drf-jwt + # edx-drf-extensions + # pyjwt pylint==3.0.3 # via # -r requirements/quality.txt @@ -260,6 +314,10 @@ pylint-plugin-utils==0.8.2 # -r requirements/quality.txt # pylint-celery # pylint-django +pymongo==3.13.0 + # via + # -r requirements/quality.txt + # edx-opaque-keys pynacl==1.5.0 # via # -r requirements/quality.txt @@ -272,7 +330,7 @@ pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt # build -pytest==7.4.3 +pytest==7.4.4 # via # -r requirements/quality.txt # pytest-cov @@ -289,6 +347,7 @@ pytz==2023.3.post1 # via # -r requirements/quality.txt # django + # djangorestframework pyyaml==6.0.1 # via # -r requirements/quality.txt @@ -298,9 +357,15 @@ readme-renderer==42.0 # via # -r requirements/quality.txt # twine +referencing==0.32.0 + # via + # -r requirements/quality.txt + # jsonschema + # jsonschema-specifications requests==2.31.0 # via # -r requirements/quality.txt + # edx-drf-extensions # requests-toolbelt # twine requests-toolbelt==1.0.0 @@ -315,13 +380,23 @@ rich==13.7.0 # via # -r requirements/quality.txt # twine +rpds-py==0.16.2 + # via + # -r requirements/quality.txt + # jsonschema + # referencing secretstorage==3.3.3 # via # -r requirements/quality.txt # keyring +semantic-version==2.10.0 + # via + # -r requirements/quality.txt + # edx-drf-extensions six==1.16.0 # via # -r requirements/quality.txt + # edx-codejail # edx-lint snowballstemmer==2.2.0 # via @@ -336,6 +411,7 @@ stevedore==5.1.0 # -r requirements/quality.txt # code-annotations # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/quality.txt @@ -366,6 +442,7 @@ typing-extensions==4.9.0 # -r requirements/quality.txt # asgiref # astroid + # edx-opaque-keys # pylint # rich urllib3==2.1.0 diff --git a/requirements/doc.txt b/requirements/doc.txt index 793120e..3d06a41 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -10,43 +10,71 @@ asgiref==3.7.2 # via # -r requirements/test.txt # django +attrs==23.2.0 + # via + # -r requirements/test.txt + # jsonschema + # referencing babel==2.14.0 # via sphinx certifi==2023.11.17 - # via requests + # via + # -r requirements/test.txt + # requests cffi==1.16.0 # via # -r requirements/test.txt + # cryptography # pynacl charset-normalizer==3.3.2 - # via requests + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt # code-annotations # edx-django-utils code-annotations==1.5.0 - # via -r requirements/test.txt -coverage[toml]==7.3.3 + # via + # -r requirements/test.txt + # edx-toggles +coverage[toml]==7.4.0 # via # -r requirements/test.txt # coverage # pytest-cov +cryptography==41.0.7 + # via + # -r requirements/test.txt + # pyjwt django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt # django-crum # django-waffle + # djangorestframework + # drf-jwt # edx-django-utils + # edx-drf-extensions + # edx-toggles django-crum==0.7.9 # via # -r requirements/test.txt # edx-django-utils + # edx-toggles django-waffle==4.1.0 # via # -r requirements/test.txt # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions doc8==1.1.1 # via -r requirements/doc.in docutils==0.19 @@ -55,20 +83,44 @@ docutils==0.19 # readme-renderer # restructuredtext-lint # sphinx +drf-jwt==1.19.2 + # via + # -r requirements/test.txt + # edx-drf-extensions +edx-codejail==3.3.3 + # via -r requirements/test.txt edx-django-utils==5.9.0 + # via + # -r requirements/test.txt + # edx-drf-extensions + # edx-toggles +edx-drf-extensions==9.0.1 # via -r requirements/test.txt +edx-opaque-keys==2.5.1 + # via + # -r requirements/test.txt + # edx-drf-extensions edx-sphinx-theme==3.1.0 # via -r requirements/doc.in +edx-toggles==5.1.0 + # via -r requirements/test.txt exceptiongroup==1.2.0 # via # -r requirements/test.txt # pytest idna==3.6 - # via requests + # via + # -r requirements/test.txt + # requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.0.0 +importlib-metadata==7.0.1 # via sphinx +importlib-resources==6.1.1 + # via + # -r requirements/test.txt + # jsonschema + # jsonschema-specifications iniconfig==2.0.0 # via # -r requirements/test.txt @@ -78,6 +130,12 @@ jinja2==3.1.2 # -r requirements/test.txt # code-annotations # sphinx +jsonschema==4.20.0 + # via -r requirements/test.txt +jsonschema-specifications==2023.12.1 + # via + # -r requirements/test.txt + # jsonschema markupsafe==2.1.3 # via # -r requirements/test.txt @@ -97,11 +155,15 @@ pbr==6.0.0 # via # -r requirements/test.txt # stevedore +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/test.txt + # jsonschema pluggy==1.3.0 # via # -r requirements/test.txt # pytest -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/test.txt # edx-django-utils @@ -114,11 +176,21 @@ pygments==2.17.2 # doc8 # readme-renderer # sphinx +pyjwt[crypto]==2.8.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via + # -r requirements/test.txt + # edx-opaque-keys pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pytest==7.4.3 +pytest==7.4.4 # via # -r requirements/test.txt # pytest-cov @@ -136,18 +208,39 @@ pytz==2023.3.post1 # -r requirements/test.txt # babel # django + # djangorestframework pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations readme-renderer==42.0 # via -r requirements/doc.in +referencing==0.32.0 + # via + # -r requirements/test.txt + # jsonschema + # jsonschema-specifications requests==2.31.0 - # via sphinx + # via + # -r requirements/test.txt + # edx-drf-extensions + # sphinx restructuredtext-lint==1.4.0 # via doc8 +rpds-py==0.16.2 + # via + # -r requirements/test.txt + # jsonschema + # referencing +semantic-version==2.10.0 + # via + # -r requirements/test.txt + # edx-drf-extensions six==1.16.0 - # via edx-sphinx-theme + # via + # -r requirements/test.txt + # edx-codejail + # edx-sphinx-theme snowballstemmer==2.2.0 # via sphinx sphinx==5.3.0 @@ -176,6 +269,7 @@ stevedore==5.1.0 # code-annotations # doc8 # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/test.txt @@ -190,7 +284,13 @@ typing-extensions==4.9.0 # via # -r requirements/test.txt # asgiref + # edx-opaque-keys urllib3==2.1.0 - # via requests + # via + # -r requirements/test.txt + # requests zipp==3.17.0 - # via importlib-metadata + # via + # -r requirements/test.txt + # importlib-metadata + # importlib-resources diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 93a9cee..0e88226 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -8,7 +8,7 @@ build==1.0.3 # via pip-tools click==8.1.7 # via pip-tools -importlib-metadata==7.0.0 +importlib-metadata==7.0.1 # via build packaging==23.2 # via build diff --git a/requirements/pip.txt b/requirements/pip.txt index 14cb99c..a4cf530 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -8,7 +8,7 @@ wheel==0.42.0 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==23.3.1 +pip==23.3.2 # via -r requirements/pip.in -setuptools==69.0.2 +setuptools==69.0.3 # via -r requirements/pip.in diff --git a/requirements/quality.txt b/requirements/quality.txt index d0dc4f3..a6dd1e6 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -12,15 +12,24 @@ astroid==3.0.2 # via # pylint # pylint-celery +attrs==23.2.0 + # via + # -r requirements/test.txt + # jsonschema + # referencing certifi==2023.11.17 - # via requests + # via + # -r requirements/test.txt + # requests cffi==1.16.0 # via # -r requirements/test.txt # cryptography # pynacl charset-normalizer==3.3.2 - # via requests + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt @@ -34,13 +43,17 @@ code-annotations==1.5.0 # via # -r requirements/test.txt # edx-lint -coverage[toml]==7.3.3 + # edx-toggles +coverage[toml]==7.4.0 # via # -r requirements/test.txt # coverage # pytest-cov cryptography==41.0.7 - # via secretstorage + # via + # -r requirements/test.txt + # pyjwt + # secretstorage dill==0.3.7 # via pylint django==3.2.23 @@ -49,33 +62,68 @@ django==3.2.23 # -r requirements/test.txt # django-crum # django-waffle + # djangorestframework + # drf-jwt # edx-django-utils + # edx-drf-extensions + # edx-toggles django-crum==0.7.9 # via # -r requirements/test.txt # edx-django-utils + # edx-toggles django-waffle==4.1.0 # via # -r requirements/test.txt # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions docutils==0.20.1 # via readme-renderer +drf-jwt==1.19.2 + # via + # -r requirements/test.txt + # edx-drf-extensions +edx-codejail==3.3.3 + # via -r requirements/test.txt edx-django-utils==5.9.0 + # via + # -r requirements/test.txt + # edx-drf-extensions + # edx-toggles +edx-drf-extensions==9.0.1 # via -r requirements/test.txt edx-lint==5.3.6 # via -r requirements/quality.in +edx-opaque-keys==2.5.1 + # via + # -r requirements/test.txt + # edx-drf-extensions +edx-toggles==5.1.0 + # via -r requirements/test.txt exceptiongroup==1.2.0 # via # -r requirements/test.txt # pytest idna==3.6 - # via requests -importlib-metadata==7.0.0 + # via + # -r requirements/test.txt + # requests +importlib-metadata==7.0.1 # via # keyring # twine importlib-resources==6.1.1 - # via keyring + # via + # -r requirements/test.txt + # jsonschema + # jsonschema-specifications + # keyring iniconfig==2.0.0 # via # -r requirements/test.txt @@ -94,6 +142,12 @@ jinja2==3.1.2 # via # -r requirements/test.txt # code-annotations +jsonschema==4.20.0 + # via -r requirements/test.txt +jsonschema-specifications==2023.12.1 + # via + # -r requirements/test.txt + # jsonschema keyring==24.3.0 # via twine markdown-it-py==3.0.0 @@ -124,13 +178,17 @@ pbr==6.0.0 # stevedore pkginfo==1.9.6 # via twine +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/test.txt + # jsonschema platformdirs==4.1.0 # via pylint pluggy==1.3.0 # via # -r requirements/test.txt # pytest -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/test.txt # edx-django-utils @@ -146,6 +204,12 @@ pygments==2.17.2 # via # readme-renderer # rich +pyjwt[crypto]==2.8.0 + # via + # -r requirements/test.txt + # drf-jwt + # edx-drf-extensions + # pyjwt pylint==3.0.3 # via # edx-lint @@ -160,11 +224,15 @@ pylint-plugin-utils==0.8.2 # via # pylint-celery # pylint-django +pymongo==3.13.0 + # via + # -r requirements/test.txt + # edx-opaque-keys pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pytest==7.4.3 +pytest==7.4.4 # via # -r requirements/test.txt # pytest-cov @@ -181,14 +249,22 @@ pytz==2023.3.post1 # via # -r requirements/test.txt # django + # djangorestframework pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations readme-renderer==42.0 # via twine +referencing==0.32.0 + # via + # -r requirements/test.txt + # jsonschema + # jsonschema-specifications requests==2.31.0 # via + # -r requirements/test.txt + # edx-drf-extensions # requests-toolbelt # twine requests-toolbelt==1.0.0 @@ -197,10 +273,22 @@ rfc3986==2.0.0 # via twine rich==13.7.0 # via twine +rpds-py==0.16.2 + # via + # -r requirements/test.txt + # jsonschema + # referencing secretstorage==3.3.3 # via keyring +semantic-version==2.10.0 + # via + # -r requirements/test.txt + # edx-drf-extensions six==1.16.0 - # via edx-lint + # via + # -r requirements/test.txt + # edx-codejail + # edx-lint snowballstemmer==2.2.0 # via pydocstyle sqlparse==0.4.4 @@ -212,6 +300,7 @@ stevedore==5.1.0 # -r requirements/test.txt # code-annotations # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/test.txt @@ -231,13 +320,16 @@ typing-extensions==4.9.0 # -r requirements/test.txt # asgiref # astroid + # edx-opaque-keys # pylint # rich urllib3==2.1.0 # via + # -r requirements/test.txt # requests # twine zipp==3.17.0 # via + # -r requirements/test.txt # importlib-metadata # importlib-resources diff --git a/requirements/scripts.txt b/requirements/scripts.txt index 90ce1d7..0847847 100644 --- a/requirements/scripts.txt +++ b/requirements/scripts.txt @@ -8,34 +8,52 @@ asgiref==3.7.2 # via # -r requirements/base.txt # django -attrs==23.1.0 - # via openedx-events +attrs==23.2.0 + # via + # -r requirements/base.txt + # jsonschema + # openedx-events + # referencing avro==1.11.3 # via confluent-kafka certifi==2023.11.17 - # via requests + # via + # -r requirements/base.txt + # requests cffi==1.16.0 # via # -r requirements/base.txt + # cryptography # pynacl charset-normalizer==3.3.2 - # via requests + # via + # -r requirements/base.txt + # requests click==8.1.7 # via # -r requirements/base.txt # code-annotations # edx-django-utils code-annotations==1.5.0 - # via edx-toggles + # via + # -r requirements/base.txt + # edx-toggles confluent-kafka[avro]==2.3.0 # via -r requirements/scripts.in +cryptography==41.0.7 + # via + # -r requirements/base.txt + # pyjwt django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt # django-crum # django-waffle + # djangorestframework + # drf-jwt # edx-django-utils + # edx-drf-extensions # edx-event-bus-kafka # edx-toggles # openedx-events @@ -48,28 +66,65 @@ django-waffle==4.1.0 # via # -r requirements/base.txt # edx-django-utils + # edx-drf-extensions # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/base.txt + # drf-jwt + # edx-drf-extensions +drf-jwt==1.19.2 + # via + # -r requirements/base.txt + # edx-drf-extensions +edx-codejail==3.3.3 + # via -r requirements/base.txt edx-django-utils==5.9.0 # via # -r requirements/base.txt + # edx-drf-extensions # edx-event-bus-kafka # edx-toggles +edx-drf-extensions==9.0.1 + # via -r requirements/base.txt edx-event-bus-kafka==5.5.0 # via -r requirements/scripts.in edx-opaque-keys[django]==2.5.1 - # via openedx-events + # via + # -r requirements/base.txt + # edx-drf-extensions + # openedx-events edx-toggles==5.1.0 - # via edx-event-bus-kafka -fastavro==1.9.1 + # via + # -r requirements/base.txt + # edx-event-bus-kafka +fastavro==1.9.2 # via # confluent-kafka # openedx-events idna==3.6 - # via requests + # via + # -r requirements/base.txt + # requests +importlib-resources==6.1.1 + # via + # -r requirements/base.txt + # jsonschema + # jsonschema-specifications jinja2==3.1.2 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations +jsonschema==4.20.0 + # via -r requirements/base.txt +jsonschema-specifications==2023.12.1 + # via + # -r requirements/base.txt + # jsonschema markupsafe==2.1.3 - # via jinja2 + # via + # -r requirements/base.txt + # jinja2 newrelic==9.3.0 # via # -r requirements/base.txt @@ -80,7 +135,11 @@ pbr==6.0.0 # via # -r requirements/base.txt # stevedore -psutil==5.9.6 +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/base.txt + # jsonschema +psutil==5.9.7 # via # -r requirements/base.txt # edx-django-utils @@ -88,22 +147,56 @@ pycparser==2.21 # via # -r requirements/base.txt # cffi +pyjwt[crypto]==2.8.0 + # via + # -r requirements/base.txt + # drf-jwt + # edx-drf-extensions + # pyjwt pymongo==3.13.0 - # via edx-opaque-keys + # via + # -r requirements/base.txt + # edx-opaque-keys pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils python-slugify==8.0.1 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations pytz==2023.3.post1 # via # -r requirements/base.txt # django + # djangorestframework pyyaml==6.0.1 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations +referencing==0.32.0 + # via + # -r requirements/base.txt + # jsonschema + # jsonschema-specifications requests==2.31.0 - # via confluent-kafka + # via + # -r requirements/base.txt + # confluent-kafka + # edx-drf-extensions +rpds-py==0.16.2 + # via + # -r requirements/base.txt + # jsonschema + # referencing +semantic-version==2.10.0 + # via + # -r requirements/base.txt + # edx-drf-extensions +six==1.16.0 + # via + # -r requirements/base.txt + # edx-codejail sqlparse==0.4.4 # via # -r requirements/base.txt @@ -115,11 +208,19 @@ stevedore==5.1.0 # edx-django-utils # edx-opaque-keys text-unidecode==1.3 - # via python-slugify + # via + # -r requirements/base.txt + # python-slugify typing-extensions==4.9.0 # via # -r requirements/base.txt # asgiref # edx-opaque-keys urllib3==2.1.0 - # via requests + # via + # -r requirements/base.txt + # requests +zipp==3.17.0 + # via + # -r requirements/base.txt + # importlib-resources diff --git a/requirements/test.txt b/requirements/test.txt index 5c89cd3..f88fdf7 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,45 +8,114 @@ asgiref==3.7.2 # via # -r requirements/base.txt # django +attrs==23.2.0 + # via + # -r requirements/base.txt + # jsonschema + # referencing +certifi==2023.11.17 + # via + # -r requirements/base.txt + # requests cffi==1.16.0 # via # -r requirements/base.txt + # cryptography # pynacl +charset-normalizer==3.3.2 + # via + # -r requirements/base.txt + # requests click==8.1.7 # via # -r requirements/base.txt # code-annotations # edx-django-utils code-annotations==1.5.0 - # via -r requirements/test.in -coverage[toml]==7.3.3 + # via + # -r requirements/base.txt + # -r requirements/test.in + # edx-toggles +coverage[toml]==7.4.0 # via # coverage # pytest-cov +cryptography==41.0.7 + # via + # -r requirements/base.txt + # pyjwt # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt # django-crum # django-waffle + # djangorestframework + # drf-jwt # edx-django-utils + # edx-drf-extensions + # edx-toggles django-crum==0.7.9 # via # -r requirements/base.txt # edx-django-utils + # edx-toggles django-waffle==4.1.0 # via # -r requirements/base.txt # edx-django-utils + # edx-drf-extensions + # edx-toggles +djangorestframework==3.14.0 + # via + # -r requirements/base.txt + # drf-jwt + # edx-drf-extensions +drf-jwt==1.19.2 + # via + # -r requirements/base.txt + # edx-drf-extensions +edx-codejail==3.3.3 + # via -r requirements/base.txt edx-django-utils==5.9.0 + # via + # -r requirements/base.txt + # edx-drf-extensions + # edx-toggles +edx-drf-extensions==9.0.1 + # via -r requirements/base.txt +edx-opaque-keys==2.5.1 + # via + # -r requirements/base.txt + # edx-drf-extensions +edx-toggles==5.1.0 # via -r requirements/base.txt exceptiongroup==1.2.0 # via pytest +idna==3.6 + # via + # -r requirements/base.txt + # requests +importlib-resources==6.1.1 + # via + # -r requirements/base.txt + # jsonschema + # jsonschema-specifications iniconfig==2.0.0 # via pytest jinja2==3.1.2 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations +jsonschema==4.20.0 + # via -r requirements/base.txt +jsonschema-specifications==2023.12.1 + # via + # -r requirements/base.txt + # jsonschema markupsafe==2.1.3 - # via jinja2 + # via + # -r requirements/base.txt + # jinja2 newrelic==9.3.0 # via # -r requirements/base.txt @@ -57,9 +126,13 @@ pbr==6.0.0 # via # -r requirements/base.txt # stevedore +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/base.txt + # jsonschema pluggy==1.3.0 # via pytest -psutil==5.9.6 +psutil==5.9.7 # via # -r requirements/base.txt # edx-django-utils @@ -67,11 +140,21 @@ pycparser==2.21 # via # -r requirements/base.txt # cffi +pyjwt[crypto]==2.8.0 + # via + # -r requirements/base.txt + # drf-jwt + # edx-drf-extensions + # pyjwt +pymongo==3.13.0 + # via + # -r requirements/base.txt + # edx-opaque-keys pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils -pytest==7.4.3 +pytest==7.4.4 # via # pytest-cov # pytest-django @@ -80,13 +163,40 @@ pytest-cov==4.1.0 pytest-django==4.7.0 # via -r requirements/test.in python-slugify==8.0.1 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations pytz==2023.3.post1 # via # -r requirements/base.txt # django + # djangorestframework pyyaml==6.0.1 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations +referencing==0.32.0 + # via + # -r requirements/base.txt + # jsonschema + # jsonschema-specifications +requests==2.31.0 + # via + # -r requirements/base.txt + # edx-drf-extensions +rpds-py==0.16.2 + # via + # -r requirements/base.txt + # jsonschema + # referencing +semantic-version==2.10.0 + # via + # -r requirements/base.txt + # edx-drf-extensions +six==1.16.0 + # via + # -r requirements/base.txt + # edx-codejail sqlparse==0.4.4 # via # -r requirements/base.txt @@ -96,8 +206,11 @@ stevedore==5.1.0 # -r requirements/base.txt # code-annotations # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 - # via python-slugify + # via + # -r requirements/base.txt + # python-slugify tomli==2.0.1 # via # coverage @@ -106,3 +219,12 @@ typing-extensions==4.9.0 # via # -r requirements/base.txt # asgiref + # edx-opaque-keys +urllib3==2.1.0 + # via + # -r requirements/base.txt + # requests +zipp==3.17.0 + # via + # -r requirements/base.txt + # importlib-resources diff --git a/setup.py b/setup.py index 0a6fb7e..4b0d2af 100644 --- a/setup.py +++ b/setup.py @@ -163,6 +163,7 @@ def is_requirement(line): "lms.djangoapp": [ "arch_experiments = edx_arch_experiments.apps:EdxArchExperimentsConfig", "config_watcher = edx_arch_experiments.config_watcher.apps:ConfigWatcher", + "codejail_service = edx_arch_experiments.codejail_service.apps:CodejailService", ], }, ) From 30043a684278161fa48346f995e692ed845cdf30 Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Wed, 10 Jan 2024 19:11:53 +0000 Subject: [PATCH 2/9] fixup! Use single-quotes for identifier string (personal preference) --- edx_arch_experiments/codejail_service/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edx_arch_experiments/codejail_service/urls.py b/edx_arch_experiments/codejail_service/urls.py index 18e6d32..a86b591 100644 --- a/edx_arch_experiments/codejail_service/urls.py +++ b/edx_arch_experiments/codejail_service/urls.py @@ -7,5 +7,5 @@ from . import views urlpatterns = [ - path('api/v0/code-exec', views.code_exec_view_v0, name="code_exec_v0"), + path('api/v0/code-exec', views.code_exec_view_v0, name='code_exec_v0'), ] From 21adbfd6d2c73a9d1572b652b4f8bf04de17c57a Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Wed, 10 Jan 2024 19:12:31 +0000 Subject: [PATCH 3/9] fixup! Move no-relays check higher in method --- .../codejail_service/views.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/edx_arch_experiments/codejail_service/views.py b/edx_arch_experiments/codejail_service/views.py index 49bba3b..a9333b9 100644 --- a/edx_arch_experiments/codejail_service/views.py +++ b/edx_arch_experiments/codejail_service/views.py @@ -104,6 +104,18 @@ def code_exec_view_v0(request): if not CODEJAIL_SERVICE_ENABLED.is_enabled(): return Response("Codejail service not enabled", status=500) + # There's a risk of getting into a loop if e.g. the CMS asks the + # LMS to run codejail executions on its behalf, and the LMS is + # *also* inadvertently configured to call the LMS (itself). + # There's no good reason to have a chain of >2 services passing + # codejail requests along, so only allow execution here if we + # aren't going to pass it along to someone else. + if getattr(settings, 'ENABLE_CODEJAIL_REST_SERVICE', False): + raise Exception( + "Refusing to run codejail request from over the network " + "when we're going to pass it to another IDA anyway" + ) + params_json = request.data['payload'] params = json.loads(params_json) jsonschema.validate(params, payload_schema) @@ -117,18 +129,6 @@ def code_exec_view_v0(request): extra_files = request.FILES - # There's a risk of getting into a loop if e.g. the CMS asks the - # LMS to run codejail executions on its behalf, and the LMS is - # *also* inadvertently configured to call the LMS (itself). - # There's no good reason to have a chain of >2 services passing - # codejail requests along, so only allow execution here if we - # aren't going to pass it along to someone else. - if getattr(settings, 'ENABLE_CODEJAIL_REST_SERVICE', False): - raise Exception( - "Refusing to run codejail request from over the network " - "when we're going to pass it to another IDA anyway" - ) - # Far too dangerous to allow unsafe executions to come in over the # network, no matter who we think the caller is. The caller is the # one who has the context on safety. From 92ffa67342d8ee105d305577d5398ea96b6802c1 Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Wed, 10 Jan 2024 19:18:15 +0000 Subject: [PATCH 4/9] fixup! Make globals_dict required --- edx_arch_experiments/codejail_service/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edx_arch_experiments/codejail_service/views.py b/edx_arch_experiments/codejail_service/views.py index a9333b9..ed79504 100644 --- a/edx_arch_experiments/codejail_service/views.py +++ b/edx_arch_experiments/codejail_service/views.py @@ -59,7 +59,7 @@ }, 'unsafely': {'type': 'boolean'}, }, - 'required': ['code'], + 'required': ['code', 'globals_dict'], } # A note on the authorization model used here: @@ -121,7 +121,7 @@ def code_exec_view_v0(request): jsonschema.validate(params, payload_schema) complete_code = params['code'] # includes standard prolog - input_globals_dict = params.get('globals_dict') or {} + input_globals_dict = params['globals_dict'] python_path = params.get('python_path') or [] limit_overrides_context = params.get('limit_overrides_context') slug = params.get('slug') From 9bb44eedab3c417ac0aae09873656ad5e678e497 Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Wed, 10 Jan 2024 19:18:30 +0000 Subject: [PATCH 5/9] fixup! Convert some exceptions into responses --- edx_arch_experiments/codejail_service/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/edx_arch_experiments/codejail_service/views.py b/edx_arch_experiments/codejail_service/views.py index ed79504..e819a75 100644 --- a/edx_arch_experiments/codejail_service/views.py +++ b/edx_arch_experiments/codejail_service/views.py @@ -111,10 +111,11 @@ def code_exec_view_v0(request): # codejail requests along, so only allow execution here if we # aren't going to pass it along to someone else. if getattr(settings, 'ENABLE_CODEJAIL_REST_SERVICE', False): - raise Exception( + log.error( "Refusing to run codejail request from over the network " "when we're going to pass it to another IDA anyway" ) + return Response("Codejail service is misconfigured. (Refusing to act as relay.)", status=500) params_json = request.data['payload'] params = json.loads(params_json) @@ -133,7 +134,7 @@ def code_exec_view_v0(request): # network, no matter who we think the caller is. The caller is the # one who has the context on safety. if unsafely: - raise Exception("Refusing to run codejail request from over the network with unsafely=true") + return Response("Refusing codejail execution with unsafely=true", status=400) output_globals_dict = deepcopy(input_globals_dict) # Output dict will be mutated by safe_exec try: From 6c02d0ead5b01645f5dfb1ec64ccfc60fa79e78f Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Thu, 11 Jan 2024 16:55:54 +0000 Subject: [PATCH 6/9] fixup! JSON for all responses, and document response format --- edx_arch_experiments/codejail_service/views.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/edx_arch_experiments/codejail_service/views.py b/edx_arch_experiments/codejail_service/views.py index e819a75..44b6c58 100644 --- a/edx_arch_experiments/codejail_service/views.py +++ b/edx_arch_experiments/codejail_service/views.py @@ -100,9 +100,16 @@ def code_exec_view_v0(request): safe_exec (aside from `extra_files`). See payload_schema for type information. This API does not permit `unsafely=true`. + + If the response is a 200, the codejail execution completed. The response + will be JSON containing either a single key `globals_dict` (containing + the global scope values at the end of a run to completion) or `emsg` (the + exception the submitted code raised). + + Other responses are errors, with a JSON body containing further details. """ if not CODEJAIL_SERVICE_ENABLED.is_enabled(): - return Response("Codejail service not enabled", status=500) + return Response({'error': "Codejail service not enabled"}, status=500) # There's a risk of getting into a loop if e.g. the CMS asks the # LMS to run codejail executions on its behalf, and the LMS is @@ -115,7 +122,7 @@ def code_exec_view_v0(request): "Refusing to run codejail request from over the network " "when we're going to pass it to another IDA anyway" ) - return Response("Codejail service is misconfigured. (Refusing to act as relay.)", status=500) + return Response({'error': "Codejail service is misconfigured. (Refusing to act as relay.)"}, status=500) params_json = request.data['payload'] params = json.loads(params_json) @@ -134,7 +141,7 @@ def code_exec_view_v0(request): # network, no matter who we think the caller is. The caller is the # one who has the context on safety. if unsafely: - return Response("Refusing codejail execution with unsafely=true", status=400) + return Response({'error': "Refusing codejail execution with unsafely=true"}, status=400) output_globals_dict = deepcopy(input_globals_dict) # Output dict will be mutated by safe_exec try: From 448bb1f61ad546203b914744f9e2052f7c4cc9af Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Thu, 11 Jan 2024 19:06:00 +0000 Subject: [PATCH 7/9] fixup! Fix file uploading and add unit tests - Add dependency on ddt - Report json errors cleanly --- .../tests/test_course_library.zip | Bin 0 -> 238 bytes .../codejail_service/tests/test_views.py | 141 ++++++++++++++++++ .../codejail_service/views.py | 20 ++- requirements/base.txt | 8 +- requirements/dev.txt | 14 +- requirements/doc.txt | 10 +- requirements/quality.txt | 12 +- requirements/scripts.txt | 10 +- requirements/test.in | 1 + requirements/test.txt | 10 +- 10 files changed, 193 insertions(+), 33 deletions(-) create mode 100644 edx_arch_experiments/codejail_service/tests/test_course_library.zip create mode 100644 edx_arch_experiments/codejail_service/tests/test_views.py diff --git a/edx_arch_experiments/codejail_service/tests/test_course_library.zip b/edx_arch_experiments/codejail_service/tests/test_course_library.zip new file mode 100644 index 0000000000000000000000000000000000000000..42409e320b5a13ec3688bb9b6606283099a17bdb GIT binary patch literal 238 zcmWIWW@Zs#U|`^2D4V7oQQqujY6j$)1F;~33`25$X;E=%d`@OkQDRZ0UO{DO2qy#c zev1XE+kv>Wf}4SnzzD=!8bK^94q}Bk2+c78 R-mGjO4U9k-4y3`l834LLK4bs@ literal 0 HcmV?d00001 diff --git a/edx_arch_experiments/codejail_service/tests/test_views.py b/edx_arch_experiments/codejail_service/tests/test_views.py new file mode 100644 index 0000000..0aa005d --- /dev/null +++ b/edx_arch_experiments/codejail_service/tests/test_views.py @@ -0,0 +1,141 @@ +""" +Test codejail service views. +""" + +import json +import textwrap +from os import path + +import ddt +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings +from django.urls import reverse +from rest_framework.test import APIClient + +from edx_arch_experiments.codejail_service import views + + +@override_settings( + ROOT_URLCONF='edx_arch_experiments.codejail_service.urls', + MIDDLEWARE=[ + 'django.contrib.sessions.middleware.SessionMiddleware', + ], +) +@ddt.ddt +class TestExecService(TestCase): + """Test the v0 code exec view.""" + + def setUp(self): + super().setUp() + user_model = get_user_model() + self.admin_user = user_model.objects.create_user('cms_worker', is_staff=True) + self.other_user = user_model.objects.create_user('student', is_staff=False) + self.standard_params = {'code': 'retval = 3 + 4', 'globals_dict': {}} + + def _test_codejail_api(self, *, user=None, files=None, skip_auth=False, params=None, exp_status, exp_body): + """ + Call the view and make assertions. + + Arguments: + user: User to authenticate as when calling view (None for unauthenticated) + exp_status: Assert that the response HTTP status code is this value + exp_body: Assert that the response body JSON is this value + """ + assert not (user and skip_auth) + + client = APIClient() + user = user or self.admin_user + if not skip_auth: + client.force_authenticate(user) + + params = self.standard_params if params is None else params + payload = json.dumps(params) + req_body = {'payload': payload, **(files or {})} + + resp = client.post(reverse('code_exec_v0'), req_body, format='multipart') + + assert resp.status_code == exp_status + assert json.loads(resp.content) == exp_body + + def test_success(self): + """Regular successful call.""" + self._test_codejail_api( + exp_status=200, exp_body={'globals_dict': {'retval': 7}}, + ) + + @override_settings(CODEJAIL_SERVICE_ENABLED=False) + def test_feature_disabled(self): + """Service can be disabled.""" + self._test_codejail_api( + exp_status=500, exp_body={'error': "Codejail service not enabled"}, + ) + + @override_settings(ENABLE_CODEJAIL_REST_SERVICE=True) + def test_misconfigured_as_relay(self): + """Don't accept codejail requests if we're going to send them elsewhere.""" + self._test_codejail_api( + exp_status=500, exp_body={'error': "Codejail service is misconfigured. (Refusing to act as relay.)"}, + ) + + def test_unauthenticated(self): + """Anonymous requests are rejected.""" + self._test_codejail_api( + skip_auth=True, + exp_status=403, exp_body={'detail': "Authentication credentials were not provided."}, + ) + + def test_unprivileged(self): + """Anonymous requests are rejected.""" + self._test_codejail_api( + user=self.other_user, + exp_status=403, exp_body={'detail': "You do not have permission to perform this action."}, + ) + + def test_unsafely(self): + """unsafely=true is rejected""" + self._test_codejail_api( + params=dict(**self.standard_params, unsafely=True), + exp_status=400, exp_body={'error': "Refusing codejail execution with unsafely=true"}, + ) + + @ddt.unpack + @ddt.data( + ({'globals_dict': {}}, 'code'), + ({'code': 'retval = 3 + 4'}, 'globals_dict'), + ({}, 'code'), + ) + def test_missing_params(self, params, missing): + """code and globals_dict are required""" + self._test_codejail_api( + params=params, + exp_status=400, exp_body={ + 'error': f"Payload JSON did not match schema: '{missing}' is a required property", + }, + ) + + def test_extra_files(self): + # "Course library" containing `course_library.triangular_number`. + # + # It's tempting to use zipfile to write to an io.BytesIO so + # that the test library is in plaintext. Django's request + # factory will indeed see that as a file to use in a multipart + # upload, but it will see it as an empty bytestring. (read() + # returns empty bytestring, while getvalue() returns the + # desired data). So instead we just have a small zip file on + # disk here. + library_path = path.join(path.dirname(__file__), 'test_course_library.zip') + + with open(library_path, 'rb') as lib_zip: + self._test_codejail_api( + params={ + 'code': textwrap.dedent(""" + from course_library import triangular_number + + result = triangular_number(6) + """), + 'globals_dict': {}, + 'python_path': ['python_lib.zip'], + }, + files={'python_lib.zip': lib_zip}, + exp_status=200, exp_body={'globals_dict': {'result': 21}}, + ) diff --git a/edx_arch_experiments/codejail_service/views.py b/edx_arch_experiments/codejail_service/views.py index 44b6c58..a67afa4 100644 --- a/edx_arch_experiments/codejail_service/views.py +++ b/edx_arch_experiments/codejail_service/views.py @@ -6,10 +6,11 @@ import logging from copy import deepcopy -import jsonschema from codejail.safe_exec import SafeExecException, safe_exec from django.conf import settings from edx_toggles.toggles import SettingToggle +from jsonschema.exceptions import best_match as json_error_best_match +from jsonschema.validators import Draft202012Validator from rest_framework.decorators import api_view, parser_classes, permission_classes from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.permissions import IsAdminUser @@ -61,6 +62,11 @@ }, 'required': ['code', 'globals_dict'], } +# Use this rather than jsonschema.validate, since that would check the schema +# every time it is called. Best to do it just once at startup. +Draft202012Validator.check_schema(payload_schema) +payload_validator = Draft202012Validator(payload_schema) + # A note on the authorization model used here: # @@ -126,7 +132,9 @@ def code_exec_view_v0(request): params_json = request.data['payload'] params = json.loads(params_json) - jsonschema.validate(params, payload_schema) + + if json_error := json_error_best_match(payload_validator.iter_errors(params)): + return Response({'error': f"Payload JSON did not match schema: {json_error.message}"}, status=400) complete_code = params['code'] # includes standard prolog input_globals_dict = params['globals_dict'] @@ -135,7 +143,9 @@ def code_exec_view_v0(request): slug = params.get('slug') unsafely = params.get('unsafely') - extra_files = request.FILES + # Convert to a list of (string, bytestring) pairs. Any duplicated file names + # are resolved as last-wins. + extra_files = [(filename, file.read()) for filename, file in request.FILES.items()] # Far too dangerous to allow unsafe executions to come in over the # network, no matter who we think the caller is. The caller is the @@ -154,8 +164,8 @@ def code_exec_view_v0(request): slug=slug, ) except SafeExecException as e: - log.debug("CodejailService execution failed with: {e!r}") + log.debug("CodejailService execution failed for {slug=} with: {e!r}") return Response({'emsg': f"Code jail execution failed: {e!r}"}) - log.debug("CodejailService execution succeeded, with globals={output_globals_dict!r}") + log.debug("CodejailService execution succeeded for {slug=}, with globals={output_globals_dict!r}") return Response({'globals_dict': output_globals_dict}) diff --git a/requirements/base.txt b/requirements/base.txt index 49f7249..9fbaedb 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -61,7 +61,7 @@ edx-django-utils==5.9.0 # -r requirements/base.in # edx-drf-extensions # edx-toggles -edx-drf-extensions==9.0.1 +edx-drf-extensions==9.1.2 # via -r requirements/base.in edx-opaque-keys==2.5.1 # via edx-drf-extensions @@ -73,7 +73,7 @@ importlib-resources==6.1.1 # via # jsonschema # jsonschema-specifications -jinja2==3.1.2 +jinja2==3.1.3 # via code-annotations jsonschema==4.20.0 # via -r requirements/base.in @@ -81,7 +81,7 @@ jsonschema-specifications==2023.12.1 # via jsonschema markupsafe==2.1.3 # via jinja2 -newrelic==9.3.0 +newrelic==9.4.0 # via edx-django-utils pbr==6.0.0 # via stevedore @@ -108,7 +108,7 @@ pytz==2023.3.post1 # djangorestframework pyyaml==6.0.1 # via code-annotations -referencing==0.32.0 +referencing==0.32.1 # via # jsonschema # jsonschema-specifications diff --git a/requirements/dev.txt b/requirements/dev.txt index c6043d6..ca87a77 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -76,6 +76,8 @@ cryptography==41.0.7 # -r requirements/quality.txt # pyjwt # secretstorage +ddt==1.7.1 + # via -r requirements/quality.txt diff-cover==8.0.2 # via -r requirements/dev.in dill==0.3.7 @@ -129,7 +131,7 @@ edx-django-utils==5.9.0 # -r requirements/quality.txt # edx-drf-extensions # edx-toggles -edx-drf-extensions==9.0.1 +edx-drf-extensions==9.1.2 # via -r requirements/quality.txt edx-i18n-tools==1.3.0 # via -r requirements/dev.in @@ -184,7 +186,7 @@ jeepney==0.8.0 # -r requirements/quality.txt # keyring # secretstorage -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/quality.txt # code-annotations @@ -199,7 +201,7 @@ keyring==24.3.0 # via # -r requirements/quality.txt # twine -lxml==5.0.0 +lxml==5.1.0 # via edx-i18n-tools markdown-it-py==3.0.0 # via @@ -217,11 +219,11 @@ mdurl==0.1.2 # via # -r requirements/quality.txt # markdown-it-py -more-itertools==10.1.0 +more-itertools==10.2.0 # via # -r requirements/quality.txt # jaraco-classes -newrelic==9.3.0 +newrelic==9.4.0 # via # -r requirements/quality.txt # edx-django-utils @@ -357,7 +359,7 @@ readme-renderer==42.0 # via # -r requirements/quality.txt # twine -referencing==0.32.0 +referencing==0.32.1 # via # -r requirements/quality.txt # jsonschema diff --git a/requirements/doc.txt b/requirements/doc.txt index 3d06a41..37a1ae6 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -48,6 +48,8 @@ cryptography==41.0.7 # via # -r requirements/test.txt # pyjwt +ddt==1.7.1 + # via -r requirements/test.txt django==3.2.23 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt @@ -94,7 +96,7 @@ edx-django-utils==5.9.0 # -r requirements/test.txt # edx-drf-extensions # edx-toggles -edx-drf-extensions==9.0.1 +edx-drf-extensions==9.1.2 # via -r requirements/test.txt edx-opaque-keys==2.5.1 # via @@ -125,7 +127,7 @@ iniconfig==2.0.0 # via # -r requirements/test.txt # pytest -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/test.txt # code-annotations @@ -140,7 +142,7 @@ markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 -newrelic==9.3.0 +newrelic==9.4.0 # via # -r requirements/test.txt # edx-django-utils @@ -215,7 +217,7 @@ pyyaml==6.0.1 # code-annotations readme-renderer==42.0 # via -r requirements/doc.in -referencing==0.32.0 +referencing==0.32.1 # via # -r requirements/test.txt # jsonschema diff --git a/requirements/quality.txt b/requirements/quality.txt index a6dd1e6..4a4ef9b 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -54,6 +54,8 @@ cryptography==41.0.7 # -r requirements/test.txt # pyjwt # secretstorage +ddt==1.7.1 + # via -r requirements/test.txt dill==0.3.7 # via pylint django==3.2.23 @@ -96,7 +98,7 @@ edx-django-utils==5.9.0 # -r requirements/test.txt # edx-drf-extensions # edx-toggles -edx-drf-extensions==9.0.1 +edx-drf-extensions==9.1.2 # via -r requirements/test.txt edx-lint==5.3.6 # via -r requirements/quality.in @@ -138,7 +140,7 @@ jeepney==0.8.0 # via # keyring # secretstorage -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/test.txt # code-annotations @@ -160,9 +162,9 @@ mccabe==0.7.0 # via pylint mdurl==0.1.2 # via markdown-it-py -more-itertools==10.1.0 +more-itertools==10.2.0 # via jaraco-classes -newrelic==9.3.0 +newrelic==9.4.0 # via # -r requirements/test.txt # edx-django-utils @@ -256,7 +258,7 @@ pyyaml==6.0.1 # code-annotations readme-renderer==42.0 # via twine -referencing==0.32.0 +referencing==0.32.1 # via # -r requirements/test.txt # jsonschema diff --git a/requirements/scripts.txt b/requirements/scripts.txt index 0847847..35ed440 100644 --- a/requirements/scripts.txt +++ b/requirements/scripts.txt @@ -85,7 +85,7 @@ edx-django-utils==5.9.0 # edx-drf-extensions # edx-event-bus-kafka # edx-toggles -edx-drf-extensions==9.0.1 +edx-drf-extensions==9.1.2 # via -r requirements/base.txt edx-event-bus-kafka==5.5.0 # via -r requirements/scripts.in @@ -98,7 +98,7 @@ edx-toggles==5.1.0 # via # -r requirements/base.txt # edx-event-bus-kafka -fastavro==1.9.2 +fastavro==1.9.3 # via # confluent-kafka # openedx-events @@ -111,7 +111,7 @@ importlib-resources==6.1.1 # -r requirements/base.txt # jsonschema # jsonschema-specifications -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/base.txt # code-annotations @@ -125,7 +125,7 @@ markupsafe==2.1.3 # via # -r requirements/base.txt # jinja2 -newrelic==9.3.0 +newrelic==9.4.0 # via # -r requirements/base.txt # edx-django-utils @@ -174,7 +174,7 @@ pyyaml==6.0.1 # via # -r requirements/base.txt # code-annotations -referencing==0.32.0 +referencing==0.32.1 # via # -r requirements/base.txt # jsonschema diff --git a/requirements/test.in b/requirements/test.in index 6797160..f2e6548 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -6,3 +6,4 @@ pytest-cov # pytest extension for code coverage statistics pytest-django # pytest extension for better Django support code-annotations # provides commands used by the pii_check make target. +ddt # data-driven tests \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt index f88fdf7..86e1fdd 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -44,6 +44,8 @@ cryptography==41.0.7 # via # -r requirements/base.txt # pyjwt +ddt==1.7.1 + # via -r requirements/test.in # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt @@ -81,7 +83,7 @@ edx-django-utils==5.9.0 # -r requirements/base.txt # edx-drf-extensions # edx-toggles -edx-drf-extensions==9.0.1 +edx-drf-extensions==9.1.2 # via -r requirements/base.txt edx-opaque-keys==2.5.1 # via @@ -102,7 +104,7 @@ importlib-resources==6.1.1 # jsonschema-specifications iniconfig==2.0.0 # via pytest -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/base.txt # code-annotations @@ -116,7 +118,7 @@ markupsafe==2.1.3 # via # -r requirements/base.txt # jinja2 -newrelic==9.3.0 +newrelic==9.4.0 # via # -r requirements/base.txt # edx-django-utils @@ -175,7 +177,7 @@ pyyaml==6.0.1 # via # -r requirements/base.txt # code-annotations -referencing==0.32.0 +referencing==0.32.1 # via # -r requirements/base.txt # jsonschema From d1e0a745d95d7dca0d875cdfaf685b2fdf5ff776 Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Thu, 11 Jan 2024 19:14:03 +0000 Subject: [PATCH 8/9] fixup! Also clean up and test reporting of sandboxed exceptions --- .../codejail_service/tests/test_views.py | 10 +++++++++- edx_arch_experiments/codejail_service/views.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/edx_arch_experiments/codejail_service/tests/test_views.py b/edx_arch_experiments/codejail_service/tests/test_views.py index 0aa005d..5ca1afc 100644 --- a/edx_arch_experiments/codejail_service/tests/test_views.py +++ b/edx_arch_experiments/codejail_service/tests/test_views.py @@ -105,7 +105,7 @@ def test_unsafely(self): ({}, 'code'), ) def test_missing_params(self, params, missing): - """code and globals_dict are required""" + """Two code and globals_dict params are required.""" self._test_codejail_api( params=params, exp_status=400, exp_body={ @@ -114,6 +114,7 @@ def test_missing_params(self, params, missing): ) def test_extra_files(self): + """Check that we can include a course library.""" # "Course library" containing `course_library.triangular_number`. # # It's tempting to use zipfile to write to an io.BytesIO so @@ -139,3 +140,10 @@ def test_extra_files(self): files={'python_lib.zip': lib_zip}, exp_status=200, exp_body={'globals_dict': {'result': 21}}, ) + + def test_exception(self): + """Report exceptions from jailed code.""" + self._test_codejail_api( + params={'code': '1/0', 'globals_dict': {}}, + exp_status=200, exp_body={'emsg': 'ZeroDivisionError: division by zero'}, + ) diff --git a/edx_arch_experiments/codejail_service/views.py b/edx_arch_experiments/codejail_service/views.py index a67afa4..209300f 100644 --- a/edx_arch_experiments/codejail_service/views.py +++ b/edx_arch_experiments/codejail_service/views.py @@ -164,8 +164,8 @@ def code_exec_view_v0(request): slug=slug, ) except SafeExecException as e: - log.debug("CodejailService execution failed for {slug=} with: {e!r}") - return Response({'emsg': f"Code jail execution failed: {e!r}"}) + log.debug("CodejailService execution failed for {slug=} with: {e}") + return Response({'emsg': str(e)}) log.debug("CodejailService execution succeeded for {slug=}, with globals={output_globals_dict!r}") return Response({'globals_dict': output_globals_dict}) From acb08dd64caeaea32fcb9aadcd032cd112d4cd9f Mon Sep 17 00:00:00 2001 From: Tim McCormack Date: Thu, 11 Jan 2024 21:16:14 +0000 Subject: [PATCH 9/9] fixup! Document remaining test util args; move files kwarg later - Move `files` kwarg to later in list to match usage - Correct the `user` arg documentation (None does not mean unauthenticated) - Update changelog date --- CHANGELOG.rst | 2 +- .../codejail_service/tests/test_views.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 99d1bcb..fce7583 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ Change Log Unreleased ~~~~~~~~~~ -[3.2.0] - 2024-01-09 +[3.2.0] - 2024-01-11 ~~~~~~~~~~~~~~~~~~~~ Added _____ diff --git a/edx_arch_experiments/codejail_service/tests/test_views.py b/edx_arch_experiments/codejail_service/tests/test_views.py index 5ca1afc..80823ac 100644 --- a/edx_arch_experiments/codejail_service/tests/test_views.py +++ b/edx_arch_experiments/codejail_service/tests/test_views.py @@ -32,12 +32,15 @@ def setUp(self): self.other_user = user_model.objects.create_user('student', is_staff=False) self.standard_params = {'code': 'retval = 3 + 4', 'globals_dict': {}} - def _test_codejail_api(self, *, user=None, files=None, skip_auth=False, params=None, exp_status, exp_body): + def _test_codejail_api(self, *, user=None, skip_auth=False, params=None, files=None, exp_status, exp_body): """ Call the view and make assertions. - Arguments: - user: User to authenticate as when calling view (None for unauthenticated) + Args: + user: User to authenticate as when calling view, defaulting to an is_staff user + skip_auth: If true, do not send authentication headers (incompatible with `user` argument) + params: Payload of codejail parameters, defaulting to a simple arithmetic check + files: Files to include in the API call, as dict of filenames to file objects exp_status: Assert that the response HTTP status code is this value exp_body: Assert that the response body JSON is this value """