add admin stuff
This commit is contained in:
@ -1,12 +1,15 @@
|
||||
"""FastAPI-Dependencies fuer Authentifizierung und Autorisierung."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import Cookie, Depends, HTTPException, Request, Response, WebSocket, status
|
||||
from fastapi import Cookie, Depends, Header, HTTPException, Request, Response, WebSocket, status
|
||||
|
||||
from app.db import get_db
|
||||
from app.rbac import user_has_role, user_has_role_for_host
|
||||
from app.security.api_tokens import TOKEN_PREFIX, hash_token, token_has_scope
|
||||
from app.security.sessions import (
|
||||
SESSION_COOKIE_NAME,
|
||||
decode_session_token,
|
||||
@ -120,3 +123,77 @@ def require_host_role(role_name: str):
|
||||
return user
|
||||
|
||||
return _dep
|
||||
|
||||
|
||||
# --- API-Tokens (siehe app/security/api_tokens.py, Migration 0005) -------------
|
||||
#
|
||||
# Tokens sind bewusst NUR fuer die Admin-/Verwaltungs-API gedacht (Hosts,
|
||||
# User, Rollen, ...), NICHT fuer SSH/RDP/SFTP-Sitzungen selbst -- die bleiben
|
||||
# ausschliesslich an eine eingeloggte, TOTP-verifizierte Browser-Session
|
||||
# gebunden. Ein Token ist nie maechtiger als sein explizit vergebener Scope,
|
||||
# auch wenn der zugehoerige User Admin ist (kein automatisches is_admin).
|
||||
# Die Tokenverwaltung selbst (/admin/tokens/*) laeuft ausschliesslich ueber
|
||||
# require_global_admin (Session), niemals ueber ein Token -- ein geleaktes
|
||||
# Token darf sich damit nicht selbst weitere/staerkere Tokens ausstellen.
|
||||
|
||||
|
||||
async def _validate_api_token(token: str, *, resource: str, action: str) -> CurrentUser | None:
|
||||
if not token.startswith(TOKEN_PREFIX):
|
||||
return None
|
||||
conn = get_db()
|
||||
token_hash = hash_token(token)
|
||||
cursor = await conn.execute(
|
||||
"SELECT at.id, at.user_id, at.scopes_json, at.expires_at, at.revoked_at, "
|
||||
"u.username, u.is_active "
|
||||
"FROM api_tokens at JOIN users u ON u.id = at.user_id WHERE at.token_hash = ?",
|
||||
(token_hash,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
token_id, user_id, scopes_json, expires_at, revoked_at, username, is_active = row
|
||||
if revoked_at is not None or not is_active:
|
||||
return None
|
||||
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
if expires_at is not None and expires_at <= now_str:
|
||||
return None
|
||||
scopes = json.loads(scopes_json)
|
||||
if not token_has_scope(scopes, resource, action):
|
||||
return None
|
||||
|
||||
await conn.execute(
|
||||
"UPDATE api_tokens SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?",
|
||||
(token_id,),
|
||||
)
|
||||
await conn.commit()
|
||||
return CurrentUser(id=user_id, username=username, is_admin=False)
|
||||
|
||||
|
||||
def require_admin_or_scope(resource: str, action: str):
|
||||
"""Dependency-Factory fuer die Admin-API: erlaubt Zugriff entweder ueber
|
||||
eine eingeloggte Admin-Session (Cookie, wie bisher) ODER ueber ein
|
||||
API-Token mit passendem Scope (Header 'Authorization: Bearer <token>').
|
||||
'write' schliesst 'read' auf derselben Ressource ein (siehe
|
||||
token_has_scope)."""
|
||||
|
||||
async def _dep(
|
||||
response: Response,
|
||||
jh_session: str | None = Cookie(default=None, alias=SESSION_COOKIE_NAME),
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> CurrentUser:
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
token = authorization.removeprefix("Bearer ").strip()
|
||||
principal = await _validate_api_token(token, resource=resource, action=action)
|
||||
if principal is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
"Ungueltiges, abgelaufenes oder nicht ausreichend berechtigtes Token",
|
||||
)
|
||||
return principal
|
||||
|
||||
user = await get_current_user(response, jh_session)
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin-Rechte erforderlich")
|
||||
return user
|
||||
|
||||
return _dep
|
||||
|
||||
Reference in New Issue
Block a user