diff --git a/app/services/deploy.py b/app/services/deploy.py index 7e8324f..c198ef1 100644 --- a/app/services/deploy.py +++ b/app/services/deploy.py @@ -23,7 +23,8 @@ PORT = "8276" def live_enabled() -> bool: - return os.getenv("DEPLOY_LIVE", "false").lower() == "true" + """DEPLOY_LIVE=true UND aktiver Login (guard.auth_ok) – ohne Login bleibt alles im Dry-Run.""" + return os.getenv("DEPLOY_LIVE", "false").lower() == "true" and guard.auth_ok() def token_present() -> bool: diff --git a/app/services/guard.py b/app/services/guard.py index 272f365..115b046 100644 --- a/app/services/guard.py +++ b/app/services/guard.py @@ -33,3 +33,10 @@ def describe() -> str: def denial(tenant_id: str) -> str: return (f"Tenant „{tenant_id}“ ist für Live-Schreibzugriffe nicht freigegeben (Env DEPLOY_ALLOWED_TENANTS = {describe()}). " "Dry-Runs sind weiterhin möglich.") + + +def auth_ok() -> bool: + """Live-Schreibzugriffe nur mit aktivem Login der Admin-Plattform (sonst wäre sie – mit Mittwald-Token im Container – für jeden offen). + Nur für lokale Experimente übersteuerbar: ALLOW_NO_AUTH_LIVE=true.""" + from app.settings import AUTH_ENABLED + return AUTH_ENABLED or os.getenv("ALLOW_NO_AUTH_LIVE", "false").lower() == "true" diff --git a/app/services/onyva_login.py b/app/services/onyva_login.py index d1c8123..ce7ea66 100644 --- a/app/services/onyva_login.py +++ b/app/services/onyva_login.py @@ -43,7 +43,7 @@ def key_present() -> bool: def writes_allowed() -> bool: - return os.getenv("OIDC_SYNC_LIVE", "false").lower() == "true" or os.getenv("DEPLOY_LIVE", "false").lower() == "true" + return (os.getenv("OIDC_SYNC_LIVE", "false").lower() == "true" or os.getenv("DEPLOY_LIVE", "false").lower() == "true") and guard.auth_ok() def base_url(db) -> str: diff --git a/app/templates/base.html b/app/templates/base.html index 983fa5d..e860b23 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -20,7 +20,7 @@
Deploy: {{ 'live' if deploy_live() else 'gesperrt · Dry-Run' }} {% set u = request.state.user if request is defined else None %} - {% if u and u.email and u.email != 'dev@local' %}{{ u.email }}
Abmelden
{% else %}Login: aus{% endif %} + {% if u and u.email and u.email != 'dev@local' %}{{ u.email }}
Abmelden
{% else %}Login aus – offen!{% endif %}
{% block body %}{% endblock %}
diff --git a/docs/RUNBOOK_PARALLELTEST.md b/docs/RUNBOOK_PARALLELTEST.md index 48549ca..7d583e5 100644 --- a/docs/RUNBOOK_PARALLELTEST.md +++ b/docs/RUNBOOK_PARALLELTEST.md @@ -150,6 +150,7 @@ Prüfen: - [ ] `https:///healthz` → `{"ok":true}` - [ ] `https:///` → Weiterleitung zu login.onyva.dev → Login → Dashboard mit **7 Tenants** (Seed). - [ ] Seitenleiste unten: „Deploy: gesperrt · Dry-Run“, „“. +- [ ] `https:///auth/me` zeigt `"auth_enabled": true`. Steht dort `false` (Seitenleiste: rot „Login aus – offen!“), ist die Plattform für jeden offen: `AUTH_ENABLED=true` (ohne Anführungszeichen) und die OIDC-Variablen prüfen. Ohne Login sind Live-Aktionen automatisch gesperrt. - Probleme: „Kein Zugriff“ → `ADMIN_ALLOWED_EMAILS`; `redirect_uri`-Fehler → Redirect-URI im Login-Client **exakt** wie in `OIDC_REDIRECT_URI`. --- diff --git a/tests/conftest.py b/tests/conftest.py index 5fbe58d..a370803 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,8 @@ import os import tempfile -os.environ["ONYSCRIBE_SKIP_DOTENV"] = "1" # .env nicht einlesen +os.environ["ONYSCRIBE_SKIP_DOTENV"] = "1" +os.environ["ALLOW_NO_AUTH_LIVE"] = "true" # Tests laufen ohne Login; der Guard wird separat getestet # .env nicht einlesen for var in ("CORTECS_API_KEY", "ONYVA_LOGIN_API_KEY", "MITTWALD_API_TOKEN", "MITTWALD_API_BASE", "DEPLOY_LIVE", "OIDC_SYNC_LIVE", "AUTH_ENABLED", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "OIDC_REDIRECT_URI", "OIDC_LOGOUT_REDIRECT_URI", "OIDC_ISSUER", "ADMIN_ALLOWED_EMAILS", "DEPLOY_ALLOWED_TENANTS", "CORE_RELEASE_TOKEN", "ADMIN_REQUIRED_ROLE", "SECRET_KEY", "APP_COOKIE_SECURE"): diff --git a/tests/test_core_contract.py b/tests/test_core_contract.py index a2f2bc6..624e441 100644 --- a/tests/test_core_contract.py +++ b/tests/test_core_contract.py @@ -441,3 +441,17 @@ def test_live_deploy_requires_admin_public_url_for_admin_source(client, live_env d = client.get(f"/api/deployments/{deploy.start('pf2', 'deploy', 't', http=FakeMW(), health=lambda *a: None, wait=True)}").json() assert d["status"] == "failed" and "Öffentliche URL der Admin-Plattform" in d["log"] assert client.get("/api/tenants/pf2/preflight").status_code == 200 + + +def test_live_actions_are_locked_without_login(client, monkeypatch): + """Ohne aktiven Login der Admin-Plattform bleibt alles im Dry-Run – auch mit DEPLOY_LIVE=true / OIDC_SYNC_LIVE=true.""" + from app.services import onyva_login + monkeypatch.setenv("DEPLOY_LIVE", "true") + monkeypatch.setenv("OIDC_SYNC_LIVE", "true") + monkeypatch.setenv("DEPLOY_ALLOWED_TENANTS", "*") + assert deploy.live_enabled() and onyva_login.writes_allowed() + monkeypatch.delenv("ALLOW_NO_AUTH_LIVE") # kein Login, keine Ausnahme + assert not deploy.live_enabled() and not onyva_login.writes_allowed() + assert client.post("/api/tenants/sprinz/deploy", json={"dry_run": False}).status_code == 403 + assert client.post("/api/tenants/sprinz/oidc/sync", json={"apply": True}).status_code == 403 + assert client.get("/api/deploy/mode").json()["live"] is False