feat(l4d2-web): add admin overlay catalog CRUD with path validation
This commit is contained in:
parent
a516402163
commit
d0614b90fb
5 changed files with 86 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ from l4d2web.cli import register_cli
|
|||
from l4d2web.config import DEFAULT_CONFIG
|
||||
from l4d2web.db import init_db
|
||||
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:
|
||||
|
|
@ -20,6 +21,7 @@ def create_app(test_config: dict[str, object] | None = None) -> Flask:
|
|||
|
||||
app.before_request(load_current_user)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(overlay_bp)
|
||||
register_cli(app)
|
||||
|
||||
@app.get("/health")
|
||||
|
|
|
|||
32
components/l4d2-web-app/src/l4d2web/routes/overlay_routes.py
Normal file
32
components/l4d2-web-app/src/l4d2web/routes/overlay_routes.py
Normal 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")
|
||||
0
components/l4d2-web-app/src/l4d2web/services/__init__.py
Normal file
0
components/l4d2-web-app/src/l4d2web/services/__init__.py
Normal file
11
components/l4d2-web-app/src/l4d2web/services/security.py
Normal file
11
components/l4d2-web-app/src/l4d2web/services/security.py
Normal 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
|
||||
41
components/l4d2-web-app/tests/test_overlays.py
Normal file
41
components/l4d2-web-app/tests/test_overlays.py
Normal 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
|
||||
Loading…
Reference in a new issue