feat(l4d2-web): add admin overlay catalog CRUD with path validation

This commit is contained in:
mwiegand 2026-04-23 01:08:41 +02:00
parent a516402163
commit d0614b90fb
No known key found for this signature in database
5 changed files with 86 additions and 0 deletions

View file

@ -7,6 +7,7 @@ from l4d2web.cli import register_cli
from l4d2web.config import DEFAULT_CONFIG from l4d2web.config import DEFAULT_CONFIG
from l4d2web.db import init_db from l4d2web.db import init_db
from l4d2web.routes.auth_routes import bp as auth_bp from l4d2web.routes.auth_routes import bp as auth_bp
from l4d2web.routes.overlay_routes import bp as overlay_bp
def create_app(test_config: dict[str, object] | None = None) -> Flask: def create_app(test_config: dict[str, object] | None = None) -> Flask:
@ -20,6 +21,7 @@ def create_app(test_config: dict[str, object] | None = None) -> Flask:
app.before_request(load_current_user) app.before_request(load_current_user)
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(overlay_bp)
register_cli(app) register_cli(app)
@app.get("/health") @app.get("/health")

View file

@ -0,0 +1,32 @@
from flask import Blueprint, Response, redirect, request
from sqlalchemy import select
from l4d2web.auth import require_admin
from l4d2web.db import session_scope
from l4d2web.models import Overlay
from l4d2web.services.security import validate_overlay_path
bp = Blueprint("overlay", __name__)
@bp.post("/admin/overlays")
@require_admin
def create_overlay() -> Response:
name = request.form.get("name", "").strip()
raw_path = request.form.get("path", "").strip()
if not name or not raw_path:
return Response("missing fields", status=400)
try:
validated_path = validate_overlay_path(raw_path)
except ValueError as exc:
return Response(str(exc), status=400)
with session_scope() as db:
existing = db.scalar(select(Overlay).where(Overlay.name == name))
if existing is not None:
return Response("overlay already exists", status=409)
db.add(Overlay(name=name, path=str(validated_path)))
return redirect("/admin/overlays")

View file

@ -0,0 +1,11 @@
from pathlib import Path
OVERLAY_ROOT = Path("/opt/l4d2/overlays").resolve()
def validate_overlay_path(raw: str) -> Path:
path = Path(raw).resolve()
if OVERLAY_ROOT not in path.parents and path != OVERLAY_ROOT:
raise ValueError("overlay path must be under /opt/l4d2/overlays")
return path

View file

@ -0,0 +1,41 @@
import pytest
from l4d2web.app import create_app
from l4d2web.auth import hash_password
from l4d2web.db import init_db, session_scope
from l4d2web.models import User
@pytest.fixture
def admin_client(tmp_path, monkeypatch):
db_url = f"sqlite:///{tmp_path/'overlay.db'}"
monkeypatch.setenv("DATABASE_URL", db_url)
app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
init_db()
with session_scope() as session:
admin = User(username="admin", password_digest=hash_password("secret"), admin=True)
session.add(admin)
session.flush()
admin_id = admin.id
client = app.test_client()
with client.session_transaction() as sess:
sess["user_id"] = admin_id
return client
def test_admin_can_create_overlay(admin_client) -> None:
response = admin_client.post(
"/admin/overlays",
data={"name": "standard", "path": "/opt/l4d2/overlays/standard"},
)
assert response.status_code == 302
def test_overlay_path_must_be_under_root(admin_client) -> None:
response = admin_client.post(
"/admin/overlays",
data={"name": "bad", "path": "/tmp/bad"},
)
assert response.status_code == 400