add admin stuff
This commit is contained in:
65
app/security/api_tokens.py
Normal file
65
app/security/api_tokens.py
Normal 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
|
||||
Reference in New Issue
Block a user