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

30 lines
1.5 KiB
Python

import uuid
from datetime import datetime
from sqlalchemy import Boolean, ForeignKey, String
from sqlalchemy.dialects.postgresql import TIMESTAMP, UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class ApiToken(Base):
"""FOUND-016: langlebiges Token für Entwicklungs-/Tooling-Zugriff auf die
API (kein Nutzer-Login-Flow) - `benutzer_id` zeigt auf einen begleitenden
Dienstkonto-Benutzer (`Benutzer.ist_dienstkonto=True`), der die dem Token
zugewiesenen Rollen trägt. Nur `token_hash` wird gespeichert (SHA-256,
kein bcrypt nötig - hochentropisches Zufallstoken statt Passwort mit
Wörterbuch-/Brute-Force-Risiko, siehe app/core/security.py), der
Klartext wird ausschließlich einmalig bei der Erzeugung zurückgegeben."""
__tablename__ = "api_token"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String, nullable=False)
token_hash: Mapped[str] = mapped_column(String, unique=True, nullable=False)
benutzer_id: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False, unique=True)
aktiv: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
erstellt_am: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
erstellt_von_benutzer_id: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False)
letzte_verwendung_am: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))