Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
233 lines
8.5 KiB
Go
233 lines
8.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// ListRoles liest alle Rollen samt ihrer Berechtigungen.
|
|
//
|
|
// Die Anzahl der Rollen ist naturgemäß klein, weshalb hier bewusst nicht
|
|
// paginiert wird.
|
|
func (repository *Repository) ListRoles(queryContext context.Context) ([]Role, error) {
|
|
const selectStatement = `
|
|
SELECT r.id, r.name, coalesce(r.description, ''), r.is_system,
|
|
coalesce(array_agg(p.name ORDER BY p.name) FILTER (WHERE p.name IS NOT NULL), '{}')
|
|
FROM roles r
|
|
LEFT JOIN role_permissions rp ON rp.role_id = r.id
|
|
LEFT JOIN permissions p ON p.id = rp.permission_id
|
|
GROUP BY r.id, r.name, r.description, r.is_system
|
|
ORDER BY r.name`
|
|
|
|
roleRows, queryError := repository.connectionPool.Query(queryContext, selectStatement)
|
|
if queryError != nil {
|
|
return nil, fmt.Errorf("die rollen konnten nicht gelesen werden: %w", queryError)
|
|
}
|
|
defer roleRows.Close()
|
|
|
|
loadedRoles := make([]Role, 0)
|
|
for roleRows.Next() {
|
|
var loadedRole Role
|
|
if scanError := roleRows.Scan(&loadedRole.ID, &loadedRole.Name, &loadedRole.Description,
|
|
&loadedRole.IsSystem, &loadedRole.Permissions); scanError != nil {
|
|
return nil, fmt.Errorf("eine rolle konnte nicht gelesen werden: %w", scanError)
|
|
}
|
|
|
|
loadedRoles = append(loadedRoles, loadedRole)
|
|
}
|
|
|
|
if rowsError := roleRows.Err(); rowsError != nil {
|
|
return nil, fmt.Errorf("die rollen konnten nicht vollständig gelesen werden: %w", rowsError)
|
|
}
|
|
|
|
return loadedRoles, nil
|
|
}
|
|
|
|
// FindRoleByID liest eine Rolle samt ihrer Berechtigungen.
|
|
func (repository *Repository) FindRoleByID(queryContext context.Context, roleID uuid.UUID) (Role, error) {
|
|
const selectStatement = `
|
|
SELECT r.id, r.name, coalesce(r.description, ''), r.is_system,
|
|
coalesce(array_agg(p.name ORDER BY p.name) FILTER (WHERE p.name IS NOT NULL), '{}')
|
|
FROM roles r
|
|
LEFT JOIN role_permissions rp ON rp.role_id = r.id
|
|
LEFT JOIN permissions p ON p.id = rp.permission_id
|
|
WHERE r.id = $1
|
|
GROUP BY r.id, r.name, r.description, r.is_system`
|
|
|
|
var loadedRole Role
|
|
scanError := repository.connectionPool.QueryRow(queryContext, selectStatement, roleID).Scan(
|
|
&loadedRole.ID, &loadedRole.Name, &loadedRole.Description, &loadedRole.IsSystem, &loadedRole.Permissions)
|
|
|
|
if errors.Is(scanError, pgx.ErrNoRows) {
|
|
return Role{}, ErrRoleNotFound
|
|
}
|
|
|
|
if scanError != nil {
|
|
return Role{}, fmt.Errorf("die rolle konnte nicht gelesen werden: %w", scanError)
|
|
}
|
|
|
|
return loadedRole, nil
|
|
}
|
|
|
|
// ListPermissions liest alle verfügbaren Berechtigungen.
|
|
func (repository *Repository) ListPermissions(queryContext context.Context) ([]Permission, error) {
|
|
permissionRows, queryError := repository.connectionPool.Query(queryContext,
|
|
"SELECT name, description FROM permissions ORDER BY name")
|
|
if queryError != nil {
|
|
return nil, fmt.Errorf("die berechtigungen konnten nicht gelesen werden: %w", queryError)
|
|
}
|
|
defer permissionRows.Close()
|
|
|
|
loadedPermissions := make([]Permission, 0)
|
|
for permissionRows.Next() {
|
|
var loadedPermission Permission
|
|
if scanError := permissionRows.Scan(&loadedPermission.Name, &loadedPermission.Description); scanError != nil {
|
|
return nil, fmt.Errorf("eine berechtigung konnte nicht gelesen werden: %w", scanError)
|
|
}
|
|
|
|
loadedPermissions = append(loadedPermissions, loadedPermission)
|
|
}
|
|
|
|
if rowsError := permissionRows.Err(); rowsError != nil {
|
|
return nil, fmt.Errorf("die berechtigungen konnten nicht vollständig gelesen werden: %w", rowsError)
|
|
}
|
|
|
|
return loadedPermissions, nil
|
|
}
|
|
|
|
// CreateRole legt eine Rolle mit den angegebenen Berechtigungen an.
|
|
func (repository *Repository) CreateRole(createContext context.Context, roleName string, description string, permissionNames []string) (uuid.UUID, error) {
|
|
databaseTransaction, transactionError := repository.connectionPool.Begin(createContext)
|
|
if transactionError != nil {
|
|
return uuid.Nil, fmt.Errorf("die transaktion konnte nicht begonnen werden: %w", transactionError)
|
|
}
|
|
defer func() { _ = databaseTransaction.Rollback(createContext) }()
|
|
|
|
var createdRoleID uuid.UUID
|
|
insertError := databaseTransaction.QueryRow(createContext,
|
|
"INSERT INTO roles (name, description, is_system) VALUES ($1, $2, FALSE) RETURNING id",
|
|
roleName, nullIfEmpty(description)).Scan(&createdRoleID)
|
|
|
|
if insertError != nil {
|
|
return uuid.Nil, fmt.Errorf("die rolle konnte nicht angelegt werden: %w", insertError)
|
|
}
|
|
|
|
if permissionError := setRolePermissionsInTransaction(createContext, databaseTransaction, createdRoleID, permissionNames); permissionError != nil {
|
|
return uuid.Nil, permissionError
|
|
}
|
|
|
|
if commitError := databaseTransaction.Commit(createContext); commitError != nil {
|
|
return uuid.Nil, fmt.Errorf("die rolle konnte nicht gespeichert werden: %w", commitError)
|
|
}
|
|
|
|
return createdRoleID, nil
|
|
}
|
|
|
|
// setRolePermissionsInTransaction ersetzt die Berechtigungen einer Rolle.
|
|
func setRolePermissionsInTransaction(setContext context.Context, databaseTransaction pgx.Tx, roleID uuid.UUID, permissionNames []string) error {
|
|
if _, deleteError := databaseTransaction.Exec(setContext,
|
|
"DELETE FROM role_permissions WHERE role_id = $1", roleID); deleteError != nil {
|
|
return fmt.Errorf("die bisherigen berechtigungen konnten nicht entfernt werden: %w", deleteError)
|
|
}
|
|
|
|
for _, permissionName := range permissionNames {
|
|
commandTag, insertError := databaseTransaction.Exec(setContext, `
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT $1, id FROM permissions WHERE name = $2
|
|
ON CONFLICT DO NOTHING`, roleID, permissionName)
|
|
|
|
if insertError != nil {
|
|
return fmt.Errorf("die berechtigung %q konnte nicht zugewiesen werden: %w", permissionName, insertError)
|
|
}
|
|
|
|
// Eine unbekannte Berechtigung wäre ein stillschweigend wirkungsloser
|
|
// Eintrag und damit eine falsche Sicherheitsannahme.
|
|
if commandTag.RowsAffected() == 0 {
|
|
return fmt.Errorf("die berechtigung %q existiert nicht", permissionName)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateRole ändert Beschreibung und Berechtigungen einer Rolle.
|
|
//
|
|
// Mitgelieferte Rollen sind unveränderlich: eine Änderung würde die Bedeutung
|
|
// bestehender Zuweisungen nachträglich verschieben.
|
|
func (repository *Repository) UpdateRole(updateContext context.Context, roleID uuid.UUID, newDescription *string, permissionNames []string) error {
|
|
existingRole, lookupError := repository.FindRoleByID(updateContext, roleID)
|
|
if lookupError != nil {
|
|
return lookupError
|
|
}
|
|
|
|
if existingRole.IsSystem {
|
|
return ErrSystemRoleImmutable
|
|
}
|
|
|
|
databaseTransaction, transactionError := repository.connectionPool.Begin(updateContext)
|
|
if transactionError != nil {
|
|
return fmt.Errorf("die transaktion konnte nicht begonnen werden: %w", transactionError)
|
|
}
|
|
defer func() { _ = databaseTransaction.Rollback(updateContext) }()
|
|
|
|
if newDescription != nil {
|
|
if _, updateError := databaseTransaction.Exec(updateContext,
|
|
"UPDATE roles SET description = $2, updated_at = now() WHERE id = $1",
|
|
roleID, nullIfEmpty(*newDescription)); updateError != nil {
|
|
return fmt.Errorf("die rolle konnte nicht geändert werden: %w", updateError)
|
|
}
|
|
}
|
|
|
|
if permissionNames != nil {
|
|
if permissionError := setRolePermissionsInTransaction(updateContext, databaseTransaction, roleID, permissionNames); permissionError != nil {
|
|
return permissionError
|
|
}
|
|
}
|
|
|
|
if commitError := databaseTransaction.Commit(updateContext); commitError != nil {
|
|
return fmt.Errorf("die änderung konnte nicht gespeichert werden: %w", commitError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteRole löscht eine Rolle.
|
|
func (repository *Repository) DeleteRole(deleteContext context.Context, roleID uuid.UUID) error {
|
|
existingRole, lookupError := repository.FindRoleByID(deleteContext, roleID)
|
|
if lookupError != nil {
|
|
return lookupError
|
|
}
|
|
|
|
if existingRole.IsSystem {
|
|
return ErrSystemRoleImmutable
|
|
}
|
|
|
|
// Eine noch zugewiesene Rolle zu löschen würde Benutzern unbemerkt Rechte
|
|
// entziehen; der Fremdschlüssel verhindert das bereits, doch die Meldung
|
|
// soll verständlich sein.
|
|
var assignmentCount int
|
|
if scanError := repository.connectionPool.QueryRow(deleteContext,
|
|
"SELECT count(*) FROM user_roles WHERE role_id = $1", roleID).Scan(&assignmentCount); scanError != nil {
|
|
return fmt.Errorf("die zuweisungen konnten nicht geprüft werden: %w", scanError)
|
|
}
|
|
|
|
if assignmentCount > 0 {
|
|
return fmt.Errorf("%w (%d Zuweisungen)", ErrRoleInUse, assignmentCount)
|
|
}
|
|
|
|
commandTag, deleteError := repository.connectionPool.Exec(deleteContext, "DELETE FROM roles WHERE id = $1", roleID)
|
|
if deleteError != nil {
|
|
return fmt.Errorf("die rolle konnte nicht gelöscht werden: %w", deleteError)
|
|
}
|
|
|
|
if commandTag.RowsAffected() == 0 {
|
|
return ErrRoleNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|