add admin stuff

This commit is contained in:
2026-08-20 10:43:19 +02:00
parent 7108d446d8
commit 91758a2701
19 changed files with 2514 additions and 85 deletions

View File

@ -0,0 +1,65 @@
"""
API-Tokens mit granularem Scope (siehe app/db/migrations/0005_api_tokens.sql).
Format: "jht_<43 zeichen urlsicherer Zufalls-String>" (secrets.token_urlsafe(32)).
Wie Recovery-Codes (app/security/totp.py) wird nur ein gepfefferter SHA-256-Hash
gespeichert -- das Klartext-Token existiert nur einmalig im Response-Body der
Erzeugung und ist danach serverseitig nicht mehr rekonstruierbar.
Scope-Modell: "<ressource>:<aktion>" mit aktion in {read, write}. "write"
impliziert automatisch "read" auf derselben Ressource (siehe token_has_scope).
"""
from __future__ import annotations
import hashlib
import secrets
TOKEN_PREFIX = "jht_"
_TOKEN_PEPPER = b"api_token_pepper"
SCOPE_RESOURCES = (
"users",
"host_groups",
"hosts",
"ssh_keys",
"roles",
"user_groups",
"audit",
)
SCOPE_ACTIONS = ("read", "write")
# Alle gueltigen "<ressource>:<aktion>"-Strings, z.B. "hosts:read".
VALID_SCOPES = frozenset(
f"{resource}:{action}" for resource in SCOPE_RESOURCES for action in SCOPE_ACTIONS
)
def generate_token() -> str:
return TOKEN_PREFIX + secrets.token_urlsafe(32)
def hash_token(token: str) -> str:
return hashlib.sha256(token.encode() + _TOKEN_PEPPER).hexdigest()
def token_prefix_for_display(token: str) -> str:
"""Kurzer, nicht-geheimer Praefix zur Wiedererkennung in der UI (kein
Rueckschluss auf das volle Token moeglich, siehe Recovery-Code-Analogie)."""
return token[: len(TOKEN_PREFIX) + 8]
def validate_scopes(scopes: list[str]) -> list[str]:
"""Wirft ValueError bei unbekannten Scopes, sonst normalisierte, deduplizierte Liste."""
unknown = sorted(set(scopes) - VALID_SCOPES)
if unknown:
raise ValueError(f"Unbekannte Scope(s): {', '.join(unknown)}")
return sorted(set(scopes))
def token_has_scope(granted_scopes: list[str], resource: str, action: str) -> bool:
"""'write' schliesst 'read' auf derselben Ressource automatisch mit ein."""
if f"{resource}:{action}" in granted_scopes:
return True
if action == "read" and f"{resource}:write" in granted_scopes:
return True
return False