From dba8e01f4bc7fd822baec84b5b1cb5b30abe4563 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Fri, 11 Oct 2024 22:34:31 -0400 Subject: [PATCH] refactor(run_submission): use a custom wrapper This wrapper will only take effect if DEBUG is False and the sandboxing submodule isn't cloned. --- docs/source/contributing/setup.rst | 5 --- pyproject.toml | 4 ++ scripts/grader_wrapper.py | 55 +++++++++++++++++++++++ tin/apps/submissions/tasks.py | 72 +++++++++++++++--------------- tin/settings/__init__.py | 10 +++++ 5 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 scripts/grader_wrapper.py diff --git a/docs/source/contributing/setup.rst b/docs/source/contributing/setup.rst index 36192406..a0b65afa 100644 --- a/docs/source/contributing/setup.rst +++ b/docs/source/contributing/setup.rst @@ -52,11 +52,6 @@ Submissions In order to actually create a submission, there are some more steps. First, you'll need to install `redis `_. -You'll also need to run some scripts to emulate the sandboxing process that goes on in production. -Run the following script:: - - pipenv run python3 scripts/create_wrappers.py - After that, you'll want to start up the development server and create a course, and an assignment in the course. After saving the assignment, you can hit "Upload grader" to add a grader - the simplest example of a grader is located in ``scripts/sample_grader.py``. diff --git a/pyproject.toml b/pyproject.toml index e70e8175..4a638871 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,6 +178,10 @@ extend-ignore-names = [ "INP001", ] +"**/management/*" = [ + "INP001", +] + [tool.ruff.format] docstring-code-format = true line-ending = "lf" diff --git a/scripts/grader_wrapper.py b/scripts/grader_wrapper.py new file mode 100644 index 00000000..a17ad94c --- /dev/null +++ b/scripts/grader_wrapper.py @@ -0,0 +1,55 @@ +"""A sample wrapper script for running python submissions. + +The text in this file is read in :func:`run_submission` and +executed if ``settings.USE_SANDBOXING`` is not ``True``. +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + + +def parse_args() -> list[str]: + parser = argparse.ArgumentParser() + parser.add_argument("--write", action="append") + parser.add_argument("--read", action="append") + # since we're not being sandboxed, we don't need to do anything + # with the grader arguments + _grader_args, submission_args = parser.parse_known_args() + + if submission_args and submission_args[0] == "--": + return submission_args[1:] + return submission_args + + +def find_python() -> str: + venv = Path("{venv_path}") + if venv.name == "None": + return "{python}" + if (python := venv / "bin" / "python").exists(): + return str(python) + return str(venv / "bin" / "python3") + + +def main() -> int: + args = parse_args() + submission_path = Path("{submission_path}") + + if submission_path.suffix != ".py": + raise NotImplementedError("Only python submissions are supported in DEBUG.") + + python = find_python() + output = subprocess.run( + [python, "--", str(submission_path), *args], + stdout=sys.stdout, + stderr=sys.stderr, + check=False, + ) + return output.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tin/apps/submissions/tasks.py b/tin/apps/submissions/tasks.py index 2a26b9f9..f82d4dec 100644 --- a/tin/apps/submissions/tasks.py +++ b/tin/apps/submissions/tasks.py @@ -4,13 +4,13 @@ import os import re import select -import shutil import signal import subprocess import sys import time import traceback from decimal import Decimal +from pathlib import Path import psutil from asgiref.sync import async_to_sync @@ -42,12 +42,12 @@ def run_submission(submission_id): ) submission_path = submission.file_path - submission_wrapper_path = submission.wrapper_file_path + submission_wrapper_path = Path(submission.wrapper_file_path) args = get_assignment_sandbox_args( - ["mkdir", "-p", "--", os.path.dirname(submission_wrapper_path)], + ["mkdir", "-p", "--", str(submission_wrapper_path.parent)], network_access=False, - whitelist=[os.path.dirname(os.path.dirname(submission_wrapper_path))], + whitelist=[str(submission_wrapper_path.parent.parent)], ) try: @@ -70,35 +70,38 @@ def run_submission(submission_id): else: # pragma: no cover python_exe = "/usr/bin/python3.10" - if not settings.DEBUG or shutil.which("bwrap") is not None: - folder_name = "sandboxed" - else: - folder_name = "testing" - - with open( - os.path.join( - settings.BASE_DIR, - "sandboxing", - "wrappers", - folder_name, - f"{submission.assignment.language}.txt", + if settings.USE_SANDBOXING: + wrapper_text = ( + Path(settings.BASE_DIR) + .joinpath( + "sandboxing", + "wrappers", + "sandboxed", + f"{submission.assignment.language}.txt", + ) + .read_text("utf-8") ) - ) as wrapper_file: - wrapper_text = wrapper_file.read().format( - has_network_access=bool(submission.assignment.has_network_access), - venv_path=( - submission.assignment.venv.path - if submission.assignment.venv_fully_created - else None - ), - submission_path=submission_path, - python=python_exe, + + elif submission.assignment.language == "P": + wrapper_text = settings.DEBUG_GRADER_WRAPPER_SCRIPT.read_text("utf-8") + else: + raise NotImplementedError( + f"Unsupported language {submission.assignment.language} in DEBUG" ) - with open(submission_wrapper_path, "w", encoding="utf-8") as f_obj: - f_obj.write(wrapper_text) + wrapper_text = wrapper_text.format( + has_network_access=bool(submission.assignment.has_network_access), + venv_path=( + submission.assignment.venv.path + if submission.assignment.venv_fully_created + else None + ), + submission_path=submission_path, + python=python_exe, + ) - os.chmod(submission_wrapper_path, 0o700) + submission_wrapper_path.write_text(wrapper_text, "utf-8") + submission_wrapper_path.chmod(0o700) except OSError: submission.grader_output = ( "An internal error occurred. Please try again.\n" @@ -126,15 +129,15 @@ def run_submission(submission_id): python_exe, "-u", grader_path, - submission_wrapper_path, + str(submission_wrapper_path), submission_path, submission.student.username, grader_log_path, ] - if not settings.DEBUG or shutil.which("firejail") is not None: + if settings.USE_SANDBOXING: whitelist = [os.path.dirname(grader_path)] - read_only = [grader_path, submission_path, os.path.dirname(submission_wrapper_path)] + read_only = [grader_path, submission_path, str(submission_wrapper_path.parent)] if submission.assignment.venv_fully_created: whitelist.append(submission.assignment.venv.path) read_only.append(submission.assignment.venv.path) @@ -147,7 +150,7 @@ def run_submission(submission_id): read_only=read_only, ) - env = dict(os.environ) + env = os.environ.copy() if submission.assignment.venv_fully_created: env.update(submission.assignment.venv.get_activation_env()) @@ -275,5 +278,4 @@ def run_submission(submission_id): submission.channel_group_name, {"type": "submission.updated"} ) - if os.path.exists(submission_wrapper_path): - os.remove(submission_wrapper_path) + submission_wrapper_path.unlink(missing_ok=True) diff --git a/tin/settings/__init__.py b/tin/settings/__init__.py index 56c9450b..526d3b37 100644 --- a/tin/settings/__init__.py +++ b/tin/settings/__init__.py @@ -12,6 +12,7 @@ from __future__ import annotations import os +from pathlib import Path # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -28,6 +29,11 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +USE_SANDBOXING = ( + not DEBUG or Path(BASE_DIR).joinpath("sandboxing", "wrappers", "sandboxed", "P.txt").exists() +) + + ALLOWED_HOSTS = [ "127.0.0.1", "localhost", @@ -305,6 +311,10 @@ VENV_FILE_SIZE_LIMIT = 1 * 1000 * 1000 * 1000 # 1 GB +# The wrapper script to use when running submissions outside of production +# We still need this so that it can handle cli arguments to the wrapper script +DEBUG_GRADER_WRAPPER_SCRIPT = Path(BASE_DIR).parent / "scripts" / "grader_wrapper.py" + # Spaces and special characters may not be handled correctly # Not importing correctly - specified directly in apps/submissions/tasks.py # as of 8/3/2022, 2022ldelwich