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.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")
|
||||||
|
|
|
||||||
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