// Package policy implementiert Core RBAC-02: die zentrale Policy- // Entscheidungs- und Durchsetzungsschicht. Regeln liegen deklarativ in der // Datenbank (Casbin-Prinzip: Policy als Modell+Regeln getrennt vom Code), // versioniert und auditierbar — anders als Casbin-Dateien im Dateisystem // (siehe "bewusst vermeiden" im Ticket). package policy import ( "context" "fmt" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/rbac" ) // Rule ist eine gewaehrte Regel: role darf permission. type Rule struct { Role rbac.Role Permission rbac.Permission } // RuleChange ist ein Eintrag der Aenderungshistorie (Akzeptanzkriterium 3). type RuleChange struct { Role rbac.Role Permission rbac.Permission Action string // "grant" oder "revoke" Actor string Version int } // Store verwaltet den deklarativen Regelsatz. IsAllowed schaut NUR in die // Datenbank — es gibt keinen hartcodierten Go-Fallback, ein Regelwechsel // wirkt sich ohne Codeaenderung sofort aus (Akzeptanzkriterium 2 / 3). type Store struct { pool *pgxpool.Pool } func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} } // IsAllowed prueft, ob role das Recht permission besitzt. Default-Deny: // existiert keine passende Regel, ist der Zugriff verboten // (Akzeptanzkriterium 2 / Pruefung 2) — es gibt keinen impliziten // "erlaubt, wenn nichts anderes gesagt"-Pfad. func (s *Store) IsAllowed(ctx context.Context, role rbac.Role, perm rbac.Permission) (bool, error) { var exists bool err := s.pool.QueryRow(ctx, ` SELECT EXISTS(SELECT 1 FROM policy_rules WHERE role = $1 AND permission = $2) `, string(role), string(perm)).Scan(&exists) if err != nil { return false, fmt.Errorf("policy pruefen: %w", err) } return exists, nil } // Grant gewaehrt role das Recht permission — deklarativ, ohne Codeaenderung // wirksam. actor wird fuer die Aenderungshistorie festgehalten. func (s *Store) Grant(ctx context.Context, role rbac.Role, perm rbac.Permission, actor string) error { return s.change(ctx, role, perm, "grant", actor, ` INSERT INTO policy_rules (role, permission, granted_by, granted_at) VALUES ($1, $2, $3, now()) ON CONFLICT (role, permission) DO UPDATE SET granted_by = $3, granted_at = now() `, string(role), string(perm), actor) } // Revoke entzieht role das Recht permission. func (s *Store) Revoke(ctx context.Context, role rbac.Role, perm rbac.Permission, actor string) error { return s.change(ctx, role, perm, "revoke", actor, ` DELETE FROM policy_rules WHERE role = $1 AND permission = $2 `, string(role), string(perm)) } func (s *Store) change(ctx context.Context, role rbac.Role, perm rbac.Permission, action, actor, sql string, args ...any) error { tx, err := s.pool.Begin(ctx) if err != nil { return fmt.Errorf("transaktion starten: %w", err) } defer func() { _ = tx.Rollback(ctx) }() if _, err := tx.Exec(ctx, sql, args...); err != nil { return fmt.Errorf("regel aendern: %w", err) } var version int if err := tx.QueryRow(ctx, ` SELECT COALESCE(MAX(version), 0) + 1 FROM policy_rule_changes WHERE role = $1 AND permission = $2 `, string(role), string(perm)).Scan(&version); err != nil { return fmt.Errorf("naechste version ermitteln: %w", err) } if _, err := tx.Exec(ctx, ` INSERT INTO policy_rule_changes (role, permission, action, actor, version, changed_at) VALUES ($1, $2, $3, $4, $5, now()) `, string(role), string(perm), action, actor, version); err != nil { return fmt.Errorf("historie schreiben: %w", err) } return tx.Commit(ctx) } // History liefert die vollstaendige, versionierte Aenderungshistorie einer // Regel (Akzeptanzkriterium 3). func (s *Store) History(ctx context.Context, role rbac.Role, perm rbac.Permission) ([]RuleChange, error) { rows, err := s.pool.Query(ctx, ` SELECT action, actor, version FROM policy_rule_changes WHERE role = $1 AND permission = $2 ORDER BY version `, string(role), string(perm)) if err != nil { return nil, fmt.Errorf("historie abfragen: %w", err) } defer rows.Close() var out []RuleChange for rows.Next() { c := RuleChange{Role: role, Permission: perm} if err := rows.Scan(&c.Action, &c.Actor, &c.Version); err != nil { return nil, fmt.Errorf("historieneintrag lesen: %w", err) } out = append(out, c) } return out, rows.Err() }