Files
timemaster/backend/tests/test_tls_admin.py
T
patrickandClaude Sonnet 5 f3ed234e56 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
2026-08-05 20:29:47 +02:00

66 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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