Files
patrickandClaude Sonnet 5 a4d4119d55
CI / backend-tests (push) Successful in 3m57s
CI / frontend-build (push) Successful in 24s
feat(auth): FOUND-016 API-Token für Entwicklungs-/Tooling-Zugriff
Rollenbasiertes, langlebiges Token neben dem normalen JWT-Login-Flow -
für automatisierte Zugriffe/Skripte. Admin wählt bei Erzeugung eine oder
mehrere Rollen, Klartext wird einmalig gezeigt (wie ein Passwort), Token
ist jederzeit deaktivierbar/reaktivierbar.

Architektur: jedes Token bekommt einen Begleit-"Dienstkonto"-Benutzer
(Benutzer.ist_dienstkonto=True, kein echter Login), der die Rollen über
die bestehende benutzer_rolle-Tabelle trägt - dadurch funktionieren
require_roles()/require_permission() und alle Audit-FKs unverändert,
keine bestehende Endpunkt-Dependency musste angepasst werden.
get_current_user() erkennt ein Token am Präfix mabea_pat_ und löst es
über den Hash (SHA-256, kein bcrypt - Token ist bereits hochentropisch)
auf den Begleit-Benutzer auf. Dienstkonten sind aus GET /benutzer
ausgeblendet, damit sie keine Auswahl-Dropdowns verunreinigen.

Neuer Admin-Tab "API-Tokens" (Gruppe System, nur Administration).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
2026-09-17 01:23:21 +02:00

135 lines
5.1 KiB
Python

import pytest
from tests.conftest import auth_header, login
@pytest.mark.asyncio
async def test_api_token_anlegen_und_verwenden(client, admin_user):
"""FOUND-016: Token wird einmalig im Klartext zurückgegeben und
funktioniert danach als Bearer-Token mit den zugewiesenen Rollen."""
admin_token = await login(client, "admin1")
erstellt = await client.post(
"/api/v1/api-tokens",
json={"name": "CI-Zugriff", "rollen": ["mitarbeiter"]},
headers=auth_header(admin_token),
)
assert erstellt.status_code == 201
body = erstellt.json()
assert body["token"].startswith("mabea_pat_")
assert body["rollen"] == ["mitarbeiter"]
klartext = body["token"]
# Funktioniert wie ein normaler Bearer-Token gegen einen mitarbeiter+-Endpunkt.
me = await client.get("/api/v1/objekte", headers=auth_header(klartext))
assert me.status_code == 200
@pytest.mark.asyncio
async def test_api_token_erscheint_nicht_in_benutzer_liste(client, admin_user):
admin_token = await login(client, "admin1")
await client.post(
"/api/v1/api-tokens", json={"name": "Verstecktes Token", "rollen": []}, headers=auth_header(admin_token)
)
liste = await client.get("/api/v1/benutzer", headers=auth_header(admin_token))
namen = {b["name"] for b in liste.json()}
assert not any("Verstecktes Token" in n for n in namen)
@pytest.mark.asyncio
async def test_api_token_deaktivieren_sperrt_zugriff(client, admin_user):
admin_token = await login(client, "admin1")
erstellt = await client.post(
"/api/v1/api-tokens",
json={"name": "Wird deaktiviert", "rollen": ["administration"]},
headers=auth_header(admin_token),
)
token_id = erstellt.json()["id"]
klartext = erstellt.json()["token"]
vorher = await client.get("/api/v1/objekte", headers=auth_header(klartext))
assert vorher.status_code == 200
deaktiviert = await client.patch(
f"/api/v1/api-tokens/{token_id}", json={"aktiv": False}, headers=auth_header(admin_token)
)
assert deaktiviert.status_code == 200
assert deaktiviert.json()["aktiv"] is False
nachher = await client.get("/api/v1/objekte", headers=auth_header(klartext))
assert nachher.status_code == 401
@pytest.mark.asyncio
async def test_api_token_reaktivieren_erlaubt_zugriff_wieder(client, admin_user):
admin_token = await login(client, "admin1")
erstellt = await client.post(
"/api/v1/api-tokens",
json={"name": "Reaktivierbar", "rollen": ["mitarbeiter"]},
headers=auth_header(admin_token),
)
token_id = erstellt.json()["id"]
klartext = erstellt.json()["token"]
await client.patch(f"/api/v1/api-tokens/{token_id}", json={"aktiv": False}, headers=auth_header(admin_token))
gesperrt = await client.get("/api/v1/objekte", headers=auth_header(klartext))
assert gesperrt.status_code == 401
await client.patch(f"/api/v1/api-tokens/{token_id}", json={"aktiv": True}, headers=auth_header(admin_token))
wieder_erlaubt = await client.get("/api/v1/objekte", headers=auth_header(klartext))
assert wieder_erlaubt.status_code == 200
@pytest.mark.asyncio
async def test_ungueltiges_api_token_wird_abgelehnt(client):
response = await client.get("/api/v1/objekte", headers=auth_header("mabea_pat_frei-erfunden"))
assert response.status_code == 401
@pytest.mark.asyncio
async def test_api_token_rollen_werden_respektiert(client, admin_user):
"""Token mit mitarbeiter-Rolle darf keine admin-only-Aktion (Objekttyp
anlegen) - dieselbe require_roles()-Prüfung wie bei echten Nutzern."""
admin_token = await login(client, "admin1")
erstellt = await client.post(
"/api/v1/api-tokens",
json={"name": "Nur Mitarbeiter", "rollen": ["mitarbeiter"]},
headers=auth_header(admin_token),
)
klartext = erstellt.json()["token"]
verboten = await client.post(
"/api/v1/objekttypen",
json={"bereich_id": 1, "name": "Test", "typ": "lager"},
headers=auth_header(klartext),
)
assert verboten.status_code == 403
@pytest.mark.asyncio
async def test_api_token_liste_und_verwaltung_nur_admin(client, mitarbeiter_user):
token = await login(client, "mitarbeiter1")
liste = await client.get("/api/v1/api-tokens", headers=auth_header(token))
assert liste.status_code == 403
erstellen = await client.post(
"/api/v1/api-tokens", json={"name": "x", "rollen": []}, headers=auth_header(token)
)
assert erstellen.status_code == 403
@pytest.mark.asyncio
async def test_api_token_liste_zeigt_rollen_und_status(client, admin_user):
admin_token = await login(client, "admin1")
await client.post(
"/api/v1/api-tokens",
json={"name": "Sichtbar", "rollen": ["materialverantwortlicher"]},
headers=auth_header(admin_token),
)
liste = await client.get("/api/v1/api-tokens", headers=auth_header(admin_token))
assert liste.status_code == 200
eintrag = next(t for t in liste.json() if t["name"] == "Sichtbar")
assert eintrag["rollen"] == ["materialverantwortlicher"]
assert eintrag["aktiv"] is True
assert eintrag["letzte_verwendung_am"] is None