diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 72b00e9..b3c0555 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -71,6 +71,24 @@ Run arbitrary code without a pre-defined problem. } ``` +### 5. Cheat Flip + +Toggle cheat mode on/off. When enabled, all code execution will return "passed" for all test cases. + +- **URL**: `/cheat-flip` +- **Method**: `POST` +- **Body**: + +```json +{ + "cheat-code": "your-raw-secret-passphrase" +} +``` + +- **Notes**: + - The secret is validated using a constant-time SHA-256 comparison. + - The state is held in-memory and resets on server restart. + --- ## Supported Languages diff --git a/alembic/env.py b/alembic/env.py index d847087..20db553 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -10,7 +10,7 @@ sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'src'))) from sqlmodel import SQLModel -from models import Problem, TestCase, Category, Tag, ProblemCategoryLink, ProblemTagLink # noqa +from models import Problem, TestCase, Category, Tag, ProblemCategoryLink, ProblemTagLink, CheatMode # noqa # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/alembic/versions/a4f2c8d9e6b7_add_cheat_mode_table.py b/alembic/versions/a4f2c8d9e6b7_add_cheat_mode_table.py new file mode 100644 index 0000000..608ec34 --- /dev/null +++ b/alembic/versions/a4f2c8d9e6b7_add_cheat_mode_table.py @@ -0,0 +1,27 @@ +"""add cheat_mode table + +Revision ID: a4f2c8d9e6b7 +Revises: 922b6da97d7e +Create Date: 2026-05-04 00:00:00.000000 +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "a4f2c8d9e6b7" +down_revision = "922b6da97d7e" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "cheat_mode", + sa.Column("id", sa.Integer(), primary_key=True, nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.text("false")), + ) + op.execute(sa.text("INSERT INTO cheat_mode (id, enabled) VALUES (1, false)")) + + +def downgrade() -> None: + op.drop_table("cheat_mode") diff --git a/bruno/prod/Cheat/Cheat Flip.bru b/bruno/prod/Cheat/Cheat Flip.bru new file mode 100644 index 0000000..eee5a3c --- /dev/null +++ b/bruno/prod/Cheat/Cheat Flip.bru @@ -0,0 +1,26 @@ +meta { + name: Cheat Flip + type: http + seq: 1 +} + +post { + url: {{code-exec-url}}/cheat-flip + body: json + auth: inherit +} + +body:json { + { + "cheat-code": "{{cheater-code}}" + } +} + +vars:pre-request { + cheater-code: cheater +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/prod/Cheat/folder.bru b/bruno/prod/Cheat/folder.bru new file mode 100644 index 0000000..ef25be6 --- /dev/null +++ b/bruno/prod/Cheat/folder.bru @@ -0,0 +1,3 @@ +meta { + name: Cheat +} diff --git a/src/core/cheat.py b/src/core/cheat.py index f8be5d8..337bea2 100644 --- a/src/core/cheat.py +++ b/src/core/cheat.py @@ -1,12 +1,8 @@ """ -Cheat mode utilities — in-memory state, security-hardened. +Cheat mode utilities — database-backed persistence. -CHEAT_MODE is held purely in memory (module-level flag) and resets on every -server restart. This is intentional: cheat mode should not silently persist -across deployments or container restarts. - -CHEAT_CODE env var holds a SHA-256 hex digest of the secret passphrase. -The raw secret is NEVER stored anywhere — only its hash is in .env. +CHEAT_MODE is persisted in the database as a single `cheat_mode` row. +The raw secret passphrase is still protected by CHEAT_CODE in .env. Setup: python3 -c "import hashlib; print(hashlib.sha256(b'YOUR_SECRET').hexdigest())" @@ -20,19 +16,23 @@ import hashlib import threading -# --------------------------------------------------------------------------- -# In-memory state — process-local, resets on restart -# --------------------------------------------------------------------------- +from sqlmodel import select +from infrastructure import SessionLocal +from models import CheatMode -# Initialize from CHEAT_MODE env var so you can pre-enable it if needed, -# but this is purely optional. Default is always False (safe). -_cheat_mode: bool = os.getenv("CHEAT_MODE", "false").lower() == "true" _lock = threading.Lock() -# --------------------------------------------------------------------------- -# Security helpers -# --------------------------------------------------------------------------- +def _get_cheat_row(session): + statement = select(CheatMode).where(CheatMode.id == 1) + cheat = session.exec(statement).first() + if cheat is None: + cheat = CheatMode(id=1, enabled=False) + session.add(cheat) + session.commit() + session.refresh(cheat) + return cheat + def _hash_secret(raw: str) -> str: """Return the SHA-256 hex digest of a raw passphrase.""" @@ -48,25 +48,23 @@ def _constant_time_verify(raw_input: str, stored_hash: str) -> bool: return hmac.compare_digest(input_hash, stored_hash) -# --------------------------------------------------------------------------- -# Public API -# --------------------------------------------------------------------------- - def is_cheat_mode() -> bool: """Return current cheat mode state (thread-safe read).""" with _lock: - return _cheat_mode + if not SessionLocal: + return False + + with SessionLocal() as session: + return _get_cheat_row(session).enabled def toggle_cheat_mode(raw_cheat_code: str) -> dict: """ Validate raw_cheat_code against the stored SHA-256 hash, then flip the - in-memory cheat mode flag. No disk writes — fast and race-condition-safe. + cheat mode state in the database. Returns: { success: bool, message: str, cheat_mode: bool | None } """ - global _cheat_mode - stored_hash = os.getenv("CHEAT_CODE") if not stored_hash: return { @@ -82,9 +80,21 @@ def toggle_cheat_mode(raw_cheat_code: str) -> dict: "cheat_mode": None, } + if not SessionLocal: + return { + "success": False, + "message": "Database is not configured for cheat mode storage.", + "cheat_mode": None, + } + with _lock: - _cheat_mode = not _cheat_mode - new_mode = _cheat_mode + with SessionLocal() as session: + cheat = _get_cheat_row(session) + cheat.enabled = not cheat.enabled + session.add(cheat) + session.commit() + session.refresh(cheat) + new_mode = cheat.enabled return { "success": True, diff --git a/src/core/executor.py b/src/core/executor.py index 6e52b9a..2146015 100644 --- a/src/core/executor.py +++ b/src/core/executor.py @@ -44,6 +44,10 @@ def _set_limits(): def execute_custom_code(code: str, lang: str) -> dict: """Execute raw code without test cases.""" + # --- Cheat mode: skip all execution and return success --- + if is_cheat_mode(): + return {"status": "success", "stdout": "Cheat mode enabled - code execution skipped", "stderr": ""} + if lang not in COMPILERS: return {"status": "error", "stdout": "", "stderr": f"Unsupported language: {lang}"} diff --git a/src/models/__init__.py b/src/models/__init__.py index c3a5a13..51342b5 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1,5 +1,5 @@ from .base import ( - Problem, Category, Tag, TestCase, + Problem, Category, Tag, TestCase, CheatMode, ProblemCategoryLink, ProblemTagLink, Riddle, Question, Choice, RiddleTagLink, QuestionTagLink, QuestionCategoryLink, diff --git a/src/models/base.py b/src/models/base.py index abe581a..eb9ec1a 100644 --- a/src/models/base.py +++ b/src/models/base.py @@ -61,6 +61,11 @@ class TestCase(SQLModel, table=True): problem: "Problem" = Relationship(back_populates="test_cases") +class CheatMode(SQLModel, table=True): + __tablename__ = "cheat_mode" + id: Optional[int] = Field(default=1, primary_key=True) + enabled: bool = Field(default=False) + class Expectation(SQLModel, table=True): __tablename__ = "expectations" id: UUID = Field(default_factory=uuid4, primary_key=True) diff --git a/src/openapi.yaml b/src/openapi.yaml index 3870048..2e24091 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -629,3 +629,35 @@ paths: description: Execution results with test case statuses '404': description: Chunk or template not found + + /cheat-flip: + post: + summary: Toggle cheat mode + description: Toggles cheat mode on/off. When enabled, all code execution test cases will pass. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [cheat-code] + properties: + cheat-code: + type: string + description: The raw secret passphrase + example: "fly-moon-to-the-me" + responses: + '200': + description: Success response with new cheat mode status + content: + application/json: + schema: + type: object + properties: + status: { type: string, example: "success" } + message: { type: string, example: "Cheat mode enabled." } + cheat_mode: { type: boolean, example: true } + '401': + description: Invalid cheat code + '500': + description: Cheat mode not configured