umbau 1.0

This commit is contained in:
2026-09-02 20:30:44 +02:00
parent afe6719f51
commit 5c95b21be7
77 changed files with 10733 additions and 1914 deletions

View File

@ -16,8 +16,7 @@ USERNAME_RE = re.compile(r"^[a-zA-Z0-9._-]{3,64}$")
HOSTNAME_LABEL_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$")
ROLE_NAME = Literal[
"ssh_connect", "rdp_connect", "file_transfer", "clipboard",
"session_recording_view", "admin_hostgroup",
"ssh_connect", "rdp_connect", "file_transfer",
# Hinzugefuegt fuer RBAC-Erweiterung 'Credentials ins RBAC-Modell' (Migration
# 0008_credentials_roles.sql): erlaubt Nicht-Admins mit dieser Rolle auf
# einer Hostgruppe gezielten Zugriff auf Zugangsdaten (SSH-Keys, RDP-
@ -25,6 +24,13 @@ ROLE_NAME = Literal[
# require_admin_scope_or_host_role und admin/routes.py (rdp-credentials,
# ssh-keys-Zuordnung, GET /admin/hosts/{id}/credentials).
"credentials_view", "credentials_manage",
# 'clipboard', 'session_recording_view' und 'admin_hostgroup' wurden mit
# Migration 0015_remove_dead_roles.sql entfernt (Teil D.6 Schritt 1,
# Umsetzungsauftrag_Sonnet5.md) -- sie waren seit 0001_initial.sql
# vergebbar, aber kein Codepfad hat sie je geprueft. Betreiberentscheidung
# (2026-08-31): Clipboard bleibt host-global (hosts.clipboard_enabled),
# Sitzungs-Playback bleibt global-admin-only, keine delegierte
# Hostgruppen-Admin-Ebene.
]
@ -58,11 +64,6 @@ class UserCreateRequest(BaseModel):
username: str = Field(min_length=3, max_length=64)
initial_password: str = Field(min_length=12, max_length=256)
is_admin: bool = False
# Nur relevant, wenn der anlegende Principal Mandanten-Admin ist: wird
# serverseitig ohnehin auf den/die erlaubten Mandanten erzwungen (siehe
# create_user() in admin/routes.py) -- ein Mandanten-Admin kann hierueber
# KEINEN User einem fremden Mandanten zuordnen. Fuer Super-Admins optional.
home_tenant_id: int | None = None
@field_validator("username")
@classmethod
@ -78,27 +79,9 @@ class UserUpdateRequest(BaseModel):
new_password: str | None = Field(default=None, min_length=12, max_length=256)
class TenantCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=128)
description: str | None = Field(default=None, max_length=1024)
class TenantUpdateRequest(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=128)
description: str | None = Field(default=None, max_length=1024)
is_active: bool | None = None
class TenantAdminAssignRequest(BaseModel):
user_id: int
class HostGroupCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=128)
description: str | None = Field(default=None, max_length=1024)
# Nur fuer Super-Admins relevant (Mandanten-Admins werden serverseitig
# ohnehin auf ihren eigenen Mandanten festgelegt, siehe admin/routes.py).
tenant_id: int | None = None
class HostGroupUpdateRequest(BaseModel):
@ -179,7 +162,6 @@ class RoleRevokeRequest(BaseModel):
class UserGroupCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=128)
description: str | None = Field(default=None, max_length=1024)
tenant_id: int | None = None
class UserGroupUpdateRequest(BaseModel):
@ -204,14 +186,29 @@ class GroupRoleRevokeRequest(BaseModel):
role_name: ROLE_NAME
class GroupCredentialGrantRequest(BaseModel):
"""Achse B (Teil D Schritt 3+5, Umsetzungsauftrag_Sonnet5.md): Freigabe
EINES Zugangsdatensatzes (SSH-Key, RDP-Zugangsdaten oder SSH-Passwort --
die Art steht im Endpunktpfad, nicht im Body, siehe app/admin/routes.py)
an eine Benutzergruppe. Struktur bewusst identisch zu
GroupRoleGrantRequest (Achse A), nur mit `credential_id` statt
`host_group_id`/`role_names` -- unterschiedliche Achse, gleiches
Vergabemuster (Gruppe + Ziel + optionales Ablaufdatum)."""
user_group_id: int
credential_id: int
expires_at: str | None = None
class GroupCredentialRevokeRequest(BaseModel):
user_group_id: int
credential_id: int
class ApiTokenCreateRequest(BaseModel):
label: str = Field(min_length=1, max_length=128)
user_id: int
scopes: list[str] = Field(min_length=1, max_length=32)
expires_at: str | None = None
# Nur fuer Super-Admins relevant -- ein Mandanten-Admin wird serverseitig
# ohnehin auf seinen eigenen Mandanten festgelegt.
tenant_id: int | None = None
class SshKeyCreateRequest(BaseModel):
@ -219,7 +216,6 @@ class SshKeyCreateRequest(BaseModel):
private_key_pem: str = Field(min_length=1, max_length=32_768)
public_key: str = Field(min_length=1, max_length=8192)
key_type: Literal["ed25519", "rsa-3072", "rsa-4096", "ca-cert"]
tenant_id: int | None = None
# Anmeldename am Zielsystem. Gehoert zum Schluessel, nicht zum Host
# (Migration 0010): ein Schluessel ist immer "der Zugang von Benutzer X".
# Fuer mehrere Konten wird je ein Schluesseleintrag angelegt.
@ -264,7 +260,6 @@ class RdpCredentialCreateRequest(BaseModel):
username: str = Field(min_length=1, max_length=128)
domain: str | None = Field(default=None, max_length=128)
password: str = Field(min_length=1, max_length=512)
tenant_id: int | None = None
class RdpCredentialUpdateRequest(BaseModel):