feat(admin): SUPER_ADMIN TLS-Zertifikat-Status + Renewal-Trigger
Neuer Router /admin/tls (SUPER_ADMIN only, AuditLog, Rate-Limit 5/hour): - GET /admin/tls/status – erkennt proxy/certbot/internal-Modus, liest Ablaufdatum via openssl x509 -enddate - POST /admin/tls/renew/certbot – ruft setup-tls.sh <domain> auf - POST /admin/tls/renew/internal – ruft setup-tls-internal.sh <hostname> [ip] auf, reloaded nginx danach Läuft mit den Root-Rechten des bestehenden timemaster.service (User=root, unverändert) - Angriffsfläche dadurch begrenzt auf SUPER_ADMIN-Auth + Domain/Hostname-Validierung (Regex, kein Shell-Interpolieren, subprocess mit Argument-Liste statt shell=True). Frontend: neuer Tab "Server / TLS" in TenantsPage – Status-Anzeige + zwei Formulare (öffentlich/intern). 3 neue Tests in test_tls_admin.py (Rollen-Gate, Status im Testcontext, Input-Validierung). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTxkZEUdfgMxZvHPiZJ8bV
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""Tests für SUPER_ADMIN TLS-Status/Renewal – Test-Server hat kein echtes
|
||||
Zertifikat, daher fällt get_tls_status() auf mode="proxy" zurück. Renewal-
|
||||
Endpunkte werden nur auf Rollen-Gate + Input-Validierung geprüft (echtes
|
||||
Skript-Ausführen ist Server-Ops, kein Unit-Test-Ziel)."""
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.security import hash_password
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
|
||||
async def _make_user(db, *, email, role, company_id=None, password="Secret123"):
|
||||
await db.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||
user = User(
|
||||
id=uuid.uuid4(), company_id=company_id, email=email,
|
||||
password_hash=hash_password(password),
|
||||
first_name=role.title(), last_name="User",
|
||||
role=UserRole(role), is_active=True,
|
||||
)
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
return user.id
|
||||
|
||||
|
||||
async def _login(client, email, password="Secret123"):
|
||||
r = await client.post("/api/v1/auth/login", json={"email": email, "password": password})
|
||||
assert r.status_code == 200, r.text
|
||||
return {"Authorization": f"Bearer {r.json()['access_token']}"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_tls_status_superadmin_only(client: AsyncClient, db_session):
|
||||
await _make_user(db_session, email="sa-tls@platform.de", role="SUPER_ADMIN")
|
||||
h = await _login(client, "sa-tls@platform.de")
|
||||
|
||||
r = await client.get("/api/v1/admin/tls/status", headers=h)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["mode"] == "proxy"
|
||||
assert r.json()["renewable"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_tls_status_forbidden_for_non_superadmin(client: AsyncClient, db_session):
|
||||
company_id = uuid.uuid4()
|
||||
from app.models.company import Company
|
||||
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||
db_session.add(Company(id=company_id, name="Acme", slug=f"acme-{company_id.hex[:8]}"))
|
||||
await db_session.commit()
|
||||
await _make_user(db_session, email="admin-tls@acme.de", role="COMPANY_ADMIN", company_id=company_id)
|
||||
h = await _login(client, "admin-tls@acme.de")
|
||||
|
||||
r = await client.get("/api/v1/admin/tls/status", headers=h)
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_tls_renew_certbot_rejects_invalid_domain(client: AsyncClient, db_session):
|
||||
await _make_user(db_session, email="sa-tls2@platform.de", role="SUPER_ADMIN")
|
||||
h = await _login(client, "sa-tls2@platform.de")
|
||||
|
||||
r = await client.post("/api/v1/admin/tls/renew/certbot", json={"domain": "; rm -rf /"}, headers=h)
|
||||
assert r.status_code == 422
|
||||
Reference in New Issue
Block a user