add admin stuff
This commit is contained in:
@ -1,25 +1,44 @@
|
||||
"""
|
||||
Administrative CRUD-API: User, Hostgruppen, Hosts, Rollenvergabe, SSH-Keys.
|
||||
Administrative CRUD-API: User, Benutzergruppen, Hostgruppen, Hosts,
|
||||
Rollenvergabe (an User UND an Benutzergruppen), SSH-Keys, API-Tokens.
|
||||
|
||||
Zustandsaendernde/-lesende Endpunkte sind entweder auf eine eingeloggte
|
||||
Admin-Session ODER ein API-Token mit passendem Scope beschraenkt
|
||||
(`require_admin_or_scope`, siehe app/auth/deps.py) und schreiben einen
|
||||
Audit-Log-Eintrag (Konzept 4.7). Die Token-Verwaltung selbst
|
||||
(/admin/tokens/*) laeuft bewusst ausschliesslich ueber `require_global_admin`
|
||||
(reine Session-Aktion, kein Token-Bypass -- Privilege-Escalation-Schutz).
|
||||
|
||||
Alle zustandsaendernden Endpunkte sind auf globale Admins beschraenkt
|
||||
(`require_global_admin`) und schreiben einen Audit-Log-Eintrag (Konzept 4.7).
|
||||
Eine feingranulare, auf `admin_hostgroup` beschraenkte Admin-Rolle ist im
|
||||
Datenmodell vorbereitet, wird hier aus Uebersichtlichkeitsgruenden aber nicht
|
||||
vollstaendig verdrahtet -- siehe TODO-Markierungen fuer den naechsten Ausbauschritt.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
|
||||
from app.auth.deps import CurrentUser, require_global_admin
|
||||
from app.auth.deps import CurrentUser, require_admin_or_scope, require_global_admin
|
||||
from app.db import get_db
|
||||
from app.models.schemas import (
|
||||
ApiTokenCreateRequest,
|
||||
GroupMemberRequest,
|
||||
GroupRoleGrantRequest,
|
||||
HostCreateRequest,
|
||||
HostGroupCreateRequest,
|
||||
RdpCredentialsRequest,
|
||||
RoleGrantRequest,
|
||||
SshKeyCreateRequest,
|
||||
UserCreateRequest,
|
||||
UserGroupCreateRequest,
|
||||
)
|
||||
from app.security.api_tokens import (
|
||||
VALID_SCOPES,
|
||||
generate_token,
|
||||
hash_token,
|
||||
token_prefix_for_display,
|
||||
validate_scopes,
|
||||
)
|
||||
from app.security.audit import verify_chain, write_audit_event
|
||||
from app.security.crypto import encrypt_secret
|
||||
@ -37,7 +56,8 @@ def _client_ip(request: Request) -> str:
|
||||
|
||||
@router.post("/users", status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
payload: UserCreateRequest, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
payload: UserCreateRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("users", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute("SELECT 1 FROM users WHERE username = ?", (payload.username,))
|
||||
@ -60,7 +80,7 @@ async def create_user(
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(admin: CurrentUser = Depends(require_global_admin)):
|
||||
async def list_users(admin: CurrentUser = Depends(require_admin_or_scope("users", "read"))):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT id, username, is_admin, is_active, totp_enrolled, created_at FROM users ORDER BY id"
|
||||
@ -77,7 +97,8 @@ async def list_users(admin: CurrentUser = Depends(require_global_admin)):
|
||||
|
||||
@router.post("/users/{user_id}/deactivate")
|
||||
async def deactivate_user(
|
||||
user_id: int, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
user_id: int, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("users", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
await conn.execute(
|
||||
@ -92,11 +113,115 @@ async def deactivate_user(
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# --- Benutzergruppen (Teams) -------------------------------------------------
|
||||
#
|
||||
# "Verbindungen mit einer Gruppe teilen" = einer Benutzergruppe ueber
|
||||
# /admin/group-roles/grant eine Rolle auf einer Hostgruppe geben -- jedes
|
||||
# aktuelle und zukuenftige Mitglied erbt diese Rolle vollstaendig (siehe
|
||||
# app/rbac.py: user_has_role() vereinigt direkte und Gruppen-Grants).
|
||||
|
||||
@router.post("/user-groups", status_code=status.HTTP_201_CREATED)
|
||||
async def create_user_group(
|
||||
payload: UserGroupCreateRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("user_groups", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT 1 FROM user_groups WHERE name = ?", (payload.name,)
|
||||
)
|
||||
if await cursor.fetchone() is not None:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Gruppenname existiert bereits")
|
||||
cursor = await conn.execute(
|
||||
"INSERT INTO user_groups (name, description) VALUES (?, ?)",
|
||||
(payload.name, payload.description),
|
||||
)
|
||||
new_id = cursor.lastrowid
|
||||
await write_audit_event(
|
||||
conn, event_type="user_group_created", user_id=admin.id, client_ip=_client_ip(request),
|
||||
details={"id": new_id, "name": payload.name},
|
||||
)
|
||||
await conn.commit()
|
||||
return {"id": new_id, "name": payload.name}
|
||||
|
||||
|
||||
@router.get("/user-groups")
|
||||
async def list_user_groups(admin: CurrentUser = Depends(require_admin_or_scope("user_groups", "read"))):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT ug.id, ug.name, ug.description, ug.created_at, COUNT(ugm.user_id) "
|
||||
"FROM user_groups ug LEFT JOIN user_group_members ugm ON ugm.user_group_id = ug.id "
|
||||
"GROUP BY ug.id ORDER BY ug.name"
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [
|
||||
{"id": r[0], "name": r[1], "description": r[2], "created_at": r[3], "member_count": r[4]}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/user-groups/{group_id}/members")
|
||||
async def list_group_members(
|
||||
group_id: int, admin: CurrentUser = Depends(require_admin_or_scope("user_groups", "read"))
|
||||
):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT u.id, u.username, ugm.added_at FROM user_group_members ugm "
|
||||
"JOIN users u ON u.id = ugm.user_id WHERE ugm.user_group_id = ? ORDER BY u.username",
|
||||
(group_id,),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [{"user_id": r[0], "username": r[1], "added_at": r[2]} for r in rows]
|
||||
|
||||
|
||||
@router.post("/user-groups/{group_id}/members", status_code=status.HTTP_201_CREATED)
|
||||
async def add_group_member(
|
||||
group_id: int, payload: GroupMemberRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("user_groups", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
group_cursor = await conn.execute("SELECT 1 FROM user_groups WHERE id = ?", (group_id,))
|
||||
if await group_cursor.fetchone() is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Gruppe nicht gefunden")
|
||||
user_cursor = await conn.execute("SELECT 1 FROM users WHERE id = ?", (payload.user_id,))
|
||||
if await user_cursor.fetchone() is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Benutzer nicht gefunden")
|
||||
|
||||
await conn.execute(
|
||||
"INSERT OR IGNORE INTO user_group_members (user_group_id, user_id, added_by) VALUES (?, ?, ?)",
|
||||
(group_id, payload.user_id, admin.id),
|
||||
)
|
||||
await write_audit_event(
|
||||
conn, event_type="user_group_member_added", user_id=admin.id, client_ip=_client_ip(request),
|
||||
details={"user_group_id": group_id, "target_user_id": payload.user_id},
|
||||
)
|
||||
await conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.delete("/user-groups/{group_id}/members/{user_id}")
|
||||
async def remove_group_member(
|
||||
group_id: int, user_id: int, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("user_groups", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
await conn.execute(
|
||||
"DELETE FROM user_group_members WHERE user_group_id = ? AND user_id = ?",
|
||||
(group_id, user_id),
|
||||
)
|
||||
await write_audit_event(
|
||||
conn, event_type="user_group_member_removed", user_id=admin.id, client_ip=_client_ip(request),
|
||||
details={"user_group_id": group_id, "target_user_id": user_id},
|
||||
)
|
||||
await conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# --- Hostgruppen -------------------------------------------------------------
|
||||
|
||||
@router.post("/host-groups", status_code=status.HTTP_201_CREATED)
|
||||
async def create_host_group(
|
||||
payload: HostGroupCreateRequest, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
payload: HostGroupCreateRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("host_groups", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
@ -113,7 +238,7 @@ async def create_host_group(
|
||||
|
||||
|
||||
@router.get("/host-groups")
|
||||
async def list_host_groups(admin: CurrentUser = Depends(require_global_admin)):
|
||||
async def list_host_groups(admin: CurrentUser = Depends(require_admin_or_scope("host_groups", "read"))):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute("SELECT id, name, description FROM host_groups ORDER BY id")
|
||||
rows = await cursor.fetchall()
|
||||
@ -124,7 +249,8 @@ async def list_host_groups(admin: CurrentUser = Depends(require_global_admin)):
|
||||
|
||||
@router.post("/hosts", status_code=status.HTTP_201_CREATED)
|
||||
async def create_host(
|
||||
payload: HostCreateRequest, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
payload: HostCreateRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("hosts", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
@ -153,7 +279,10 @@ async def create_host(
|
||||
|
||||
|
||||
@router.get("/hosts")
|
||||
async def list_hosts(host_group_id: int | None = None, admin: CurrentUser = Depends(require_global_admin)):
|
||||
async def list_hosts(
|
||||
host_group_id: int | None = None,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("hosts", "read")),
|
||||
):
|
||||
conn = get_db()
|
||||
if host_group_id is not None:
|
||||
cursor = await conn.execute(
|
||||
@ -177,7 +306,8 @@ async def list_hosts(host_group_id: int | None = None, admin: CurrentUser = Depe
|
||||
|
||||
@router.post("/hosts/{host_id}/discover-host-key")
|
||||
async def discover_host_key(
|
||||
host_id: int, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
host_id: int, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("hosts", "write")),
|
||||
):
|
||||
"""ACHTUNG: Verbindet einmalig OHNE Host-Key-Pruefung, um den Fingerprint zu
|
||||
erfassen (bewusste Trust-Entscheidung, siehe Konzept 4.2). Danach gilt fuer
|
||||
@ -196,7 +326,7 @@ async def discover_host_key(
|
||||
@router.put("/hosts/{host_id}/rdp-credentials")
|
||||
async def set_rdp_credentials(
|
||||
host_id: int, payload: RdpCredentialsRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_global_admin),
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("hosts", "write")),
|
||||
):
|
||||
"""Speichert/rotiert das RDP-Passwort fuer einen Host, verschluesselt mit
|
||||
dem KEK (eigener AAD-Kontext, siehe app/security/crypto.py)."""
|
||||
@ -217,11 +347,12 @@ async def set_rdp_credentials(
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# --- Rollenvergabe -------------------------------------------------------------
|
||||
# --- Rollenvergabe (an einzelne User) -----------------------------------------
|
||||
|
||||
@router.post("/roles/grant")
|
||||
async def grant_role(
|
||||
payload: RoleGrantRequest, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
payload: RoleGrantRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("roles", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
role_cursor = await conn.execute("SELECT id FROM roles WHERE name = ?", (payload.role_name,))
|
||||
@ -247,7 +378,8 @@ async def grant_role(
|
||||
|
||||
@router.post("/roles/revoke")
|
||||
async def revoke_role(
|
||||
payload: RoleGrantRequest, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
payload: RoleGrantRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("roles", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
role_cursor = await conn.execute("SELECT id FROM roles WHERE name = ?", (payload.role_name,))
|
||||
@ -270,11 +402,114 @@ async def revoke_role(
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/roles")
|
||||
async def list_role_grants(admin: CurrentUser = Depends(require_admin_or_scope("roles", "read"))):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"""
|
||||
SELECT u.id, u.username, hg.id, hg.name, r.name, uhr.granted_by, uhr.granted_at, uhr.expires_at
|
||||
FROM user_hostgroup_roles uhr
|
||||
JOIN users u ON u.id = uhr.user_id
|
||||
JOIN host_groups hg ON hg.id = uhr.host_group_id
|
||||
JOIN roles r ON r.id = uhr.role_id
|
||||
ORDER BY u.username, hg.name, r.name
|
||||
"""
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [
|
||||
{
|
||||
"user_id": r[0], "username": r[1], "host_group_id": r[2], "host_group_name": r[3],
|
||||
"role_name": r[4], "granted_by": r[5], "granted_at": r[6], "expires_at": r[7],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
# --- Rollenvergabe (an Benutzergruppen) ---------------------------------------
|
||||
|
||||
@router.post("/group-roles/grant")
|
||||
async def grant_group_role(
|
||||
payload: GroupRoleGrantRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("roles", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
role_cursor = await conn.execute("SELECT id FROM roles WHERE name = ?", (payload.role_name,))
|
||||
role_row = await role_cursor.fetchone()
|
||||
if role_row is None:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Unbekannte Rolle")
|
||||
|
||||
await conn.execute(
|
||||
"INSERT OR REPLACE INTO group_hostgroup_roles "
|
||||
"(user_group_id, host_group_id, role_id, granted_by, expires_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(payload.user_group_id, payload.host_group_id, role_row[0], admin.id, payload.expires_at),
|
||||
)
|
||||
await write_audit_event(
|
||||
conn, event_type="group_role_granted", user_id=admin.id, client_ip=_client_ip(request),
|
||||
details={
|
||||
"user_group_id": payload.user_group_id, "host_group_id": payload.host_group_id,
|
||||
"role": payload.role_name, "expires_at": payload.expires_at,
|
||||
},
|
||||
)
|
||||
await conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/group-roles/revoke")
|
||||
async def revoke_group_role(
|
||||
payload: GroupRoleGrantRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("roles", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
role_cursor = await conn.execute("SELECT id FROM roles WHERE name = ?", (payload.role_name,))
|
||||
role_row = await role_cursor.fetchone()
|
||||
if role_row is None:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Unbekannte Rolle")
|
||||
|
||||
await conn.execute(
|
||||
"DELETE FROM group_hostgroup_roles WHERE user_group_id = ? AND host_group_id = ? AND role_id = ?",
|
||||
(payload.user_group_id, payload.host_group_id, role_row[0]),
|
||||
)
|
||||
await write_audit_event(
|
||||
conn, event_type="group_role_revoked", user_id=admin.id, client_ip=_client_ip(request),
|
||||
details={
|
||||
"user_group_id": payload.user_group_id, "host_group_id": payload.host_group_id,
|
||||
"role": payload.role_name,
|
||||
},
|
||||
)
|
||||
await conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/group-roles")
|
||||
async def list_group_role_grants(admin: CurrentUser = Depends(require_admin_or_scope("roles", "read"))):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"""
|
||||
SELECT ug.id, ug.name, hg.id, hg.name, r.name, ghr.granted_by, ghr.granted_at, ghr.expires_at
|
||||
FROM group_hostgroup_roles ghr
|
||||
JOIN user_groups ug ON ug.id = ghr.user_group_id
|
||||
JOIN host_groups hg ON hg.id = ghr.host_group_id
|
||||
JOIN roles r ON r.id = ghr.role_id
|
||||
ORDER BY ug.name, hg.name, r.name
|
||||
"""
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [
|
||||
{
|
||||
"user_group_id": r[0], "user_group_name": r[1], "host_group_id": r[2],
|
||||
"host_group_name": r[3], "role_name": r[4], "granted_by": r[5],
|
||||
"granted_at": r[6], "expires_at": r[7],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
# --- SSH-Keyverwaltung ---------------------------------------------------------
|
||||
|
||||
@router.post("/ssh-keys", status_code=status.HTTP_201_CREATED)
|
||||
async def create_ssh_key(
|
||||
payload: SshKeyCreateRequest, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
payload: SshKeyCreateRequest, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("ssh_keys", "write")),
|
||||
):
|
||||
"""Nimmt einen privaten Schluessel entgegen, verschluesselt ihn sofort mit
|
||||
dem KEK (AES-256-GCM) und haelt den Klartext nur fuer die Dauer dieses
|
||||
@ -295,9 +530,27 @@ async def create_ssh_key(
|
||||
return {"id": new_id, "label": payload.label}
|
||||
|
||||
|
||||
@router.get("/ssh-keys")
|
||||
async def list_ssh_keys(admin: CurrentUser = Depends(require_admin_or_scope("ssh_keys", "read"))):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT id, label, owner_user_id, key_type, created_at, rotated_at, expires_at "
|
||||
"FROM ssh_keys ORDER BY id"
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [
|
||||
{
|
||||
"id": r[0], "label": r[1], "owner_user_id": r[2], "key_type": r[3],
|
||||
"created_at": r[4], "rotated_at": r[5], "expires_at": r[6],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.post("/hosts/{host_id}/ssh-keys/{key_id}")
|
||||
async def map_ssh_key_to_host(
|
||||
host_id: int, key_id: int, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
host_id: int, key_id: int, request: Request,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("ssh_keys", "write")),
|
||||
):
|
||||
conn = get_db()
|
||||
await conn.execute(
|
||||
@ -312,11 +565,97 @@ async def map_ssh_key_to_host(
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# --- API-Tokens ------------------------------------------------------------
|
||||
#
|
||||
# Bewusst ausschliesslich ueber require_global_admin (Session), NIE ueber
|
||||
# require_admin_or_scope -- ein geleaktes Token darf sich damit nicht selbst
|
||||
# weitere/staerkere Tokens ausstellen (Privilege-Escalation-Schutz).
|
||||
|
||||
@router.get("/scopes")
|
||||
async def list_valid_scopes(admin: CurrentUser = Depends(require_global_admin)):
|
||||
return {"scopes": sorted(VALID_SCOPES)}
|
||||
|
||||
|
||||
@router.post("/tokens", status_code=status.HTTP_201_CREATED)
|
||||
async def create_api_token(
|
||||
payload: ApiTokenCreateRequest, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
):
|
||||
try:
|
||||
scopes = validate_scopes(payload.scopes)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
|
||||
|
||||
conn = get_db()
|
||||
user_cursor = await conn.execute("SELECT 1 FROM users WHERE id = ?", (payload.user_id,))
|
||||
if await user_cursor.fetchone() is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Benutzer nicht gefunden")
|
||||
|
||||
token = generate_token()
|
||||
cursor = await conn.execute(
|
||||
"INSERT INTO api_tokens (user_id, label, token_hash, token_prefix, scopes_json, "
|
||||
"created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
payload.user_id, payload.label, hash_token(token), token_prefix_for_display(token),
|
||||
json.dumps(scopes), admin.id, payload.expires_at,
|
||||
),
|
||||
)
|
||||
new_id = cursor.lastrowid
|
||||
await write_audit_event(
|
||||
conn, event_type="api_token_created", user_id=admin.id, client_ip=_client_ip(request),
|
||||
details={
|
||||
"token_id": new_id, "target_user_id": payload.user_id, "label": payload.label,
|
||||
"scopes": scopes, "expires_at": payload.expires_at,
|
||||
},
|
||||
)
|
||||
await conn.commit()
|
||||
# Klartext-Token wird NUR in dieser einen Response zurueckgegeben -- danach
|
||||
# ist nur noch der Hash in der DB, das Token ist nicht mehr rekonstruierbar.
|
||||
return {"id": new_id, "token": token, "prefix": token_prefix_for_display(token), "scopes": scopes}
|
||||
|
||||
|
||||
@router.get("/tokens")
|
||||
async def list_api_tokens(admin: CurrentUser = Depends(require_global_admin)):
|
||||
conn = get_db()
|
||||
cursor = await conn.execute(
|
||||
"SELECT t.id, t.user_id, u.username, t.label, t.token_prefix, t.scopes_json, "
|
||||
"t.created_at, t.expires_at, t.last_used_at, t.revoked_at "
|
||||
"FROM api_tokens t JOIN users u ON u.id = t.user_id ORDER BY t.id DESC"
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [
|
||||
{
|
||||
"id": r[0], "user_id": r[1], "username": r[2], "label": r[3], "prefix": r[4],
|
||||
"scopes": json.loads(r[5]), "created_at": r[6], "expires_at": r[7],
|
||||
"last_used_at": r[8], "revoked_at": r[9],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.post("/tokens/{token_id}/revoke")
|
||||
async def revoke_api_token(
|
||||
token_id: int, request: Request, admin: CurrentUser = Depends(require_global_admin)
|
||||
):
|
||||
conn = get_db()
|
||||
await conn.execute(
|
||||
"UPDATE api_tokens SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') "
|
||||
"WHERE id = ? AND revoked_at IS NULL",
|
||||
(token_id,),
|
||||
)
|
||||
await write_audit_event(
|
||||
conn, event_type="api_token_revoked", user_id=admin.id, client_ip=_client_ip(request),
|
||||
details={"token_id": token_id},
|
||||
)
|
||||
await conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# --- Audit-Log -----------------------------------------------------------------
|
||||
|
||||
@router.get("/audit-log")
|
||||
async def get_audit_log(
|
||||
limit: int = 100, offset: int = 0, admin: CurrentUser = Depends(require_global_admin)
|
||||
limit: int = 100, offset: int = 0,
|
||||
admin: CurrentUser = Depends(require_admin_or_scope("audit", "read")),
|
||||
):
|
||||
limit = max(1, min(limit, 1000))
|
||||
conn = get_db()
|
||||
@ -333,7 +672,7 @@ async def get_audit_log(
|
||||
|
||||
|
||||
@router.get("/audit-log/verify")
|
||||
async def verify_audit_log(admin: CurrentUser = Depends(require_global_admin)):
|
||||
async def verify_audit_log(admin: CurrentUser = Depends(require_admin_or_scope("audit", "read"))):
|
||||
"""Prueft die Hash-Chain auf Manipulationsfreiheit (Konzept 6.1/4.7)."""
|
||||
conn = get_db()
|
||||
intact, broken_at = await verify_chain(conn)
|
||||
|
||||
Reference in New Issue
Block a user