internal/policy: deklarativer, DB-gehaltener Regelsatz (policy_rules) statt hartcodierter Go-Entscheidungslogik — Store.IsAllowed schaut ausschliesslich in die Datenbank, kein Go-Fallback. Ein Regelwechsel (Grant/Revoke) wirkt sich sofort aus, ohne Codeaenderung/Deploy (Akzeptanzkriterium 3). Jede Aenderung wird atomar mit einem versionierten Historieneintrag in policy_rule_changes festgehalten (grant/revoke, Akteur, Version). Enforcer.Authorize ist Default-Deny: existiert keine Regel fuer role+ permission, ist der Zugriff verboten (Akzeptanzkriterium 2), fuer sich genommen ohne Anwendungslogik testbar. Guard/GuardTenantScoped sind die zentrale Enforcement-Funktion (Akzeptanzkriterium 1): die uebergebene Query-Funktion wird NUR bei erfolgreicher Autorisierung aufgerufen — es gibt keinen Weg, Daten ohne vorherige Authorize-Entscheidung zu erhalten. GuardTenantScoped erzwingt zusaetzlich per Funktionssignatur, dass tenantSlug TEIL der Query-Funktion ist (Akzeptanzkriterium 3) — ein nachgelagerter Post-Filter (der archivmail-Fehler aus "Bekannte Fehler vermeiden": Tenant-Filter nach statt in der Query) ist mit dieser Signatur strukturell nicht moeglich, da die Repository-Implementierung tenantSlug selbst fuer ihre eigene WHERE-Klausel entgegennimmt. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Kein Datenzugriffs-Pfad umgeht die zentrale Enforcement-Schicht — TestGuard_NeverCallsQueryWithoutAuthorization: query-Funktion wird nachweislich NICHT aufgerufen ohne vorherige Regel, erst nach Grant. PASS. 2. Anfrage ohne passende Policy wird zuverlaessig abgewiesen (Default-Deny) — TestAuthorize_DefaultDeny: keine Regel konfiguriert -> ErrDenied, nicht automatisch erlaubt. PASS. 3. Policy-Regelsatz versioniert, Regelwechsel ohne Codeaenderung nachvollziehbar — TestGrantRevoke_ChangesBehaviorWithoutCodeChange: Verhalten aendert sich durch reinen Datenbank-Grant/Revoke, Historie zeigt beide Versionen korrekt. PASS. Zusaetzlich: TestGuardTenantScoped_IsolatesDataBetweenTenants belegt das Tenant-Scoping-Muster aus Akzeptanzkriterium 3 konkret anhand zweier Tenants. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
2.5 KiB
Go
68 lines
2.5 KiB
Go
package policy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
|
|
)
|
|
|
|
// ErrDenied wird geliefert, wenn keine Regel role+permission erlaubt —
|
|
// Default-Deny (Akzeptanzkriterium 2).
|
|
var ErrDenied = errors.New("policy: zugriff verweigert")
|
|
|
|
// Enforcer ist die EINE zentrale Entscheidungs- und Durchsetzungsschicht
|
|
// (Akzeptanzkriterium 1). Repository-/Query-Code ruft ausschliesslich Guard
|
|
// bzw. GuardTenantScoped auf, nie eine Rohabfrage direkt.
|
|
type Enforcer struct {
|
|
store *Store
|
|
}
|
|
|
|
func NewEnforcer(store *Store) *Enforcer {
|
|
return &Enforcer{store: store}
|
|
}
|
|
|
|
// Authorize entscheidet erlaubt/verboten — unabhaengig von jeder konkreten
|
|
// Query, rein anhand der deklarativen Regeln (Akzeptanzkriterium 2: fuer
|
|
// sich genommen testbar, ohne Anwendungslogik).
|
|
func (e *Enforcer) Authorize(ctx context.Context, role rbac.Role, perm rbac.Permission) error {
|
|
allowed, err := e.store.IsAllowed(ctx, role, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !allowed {
|
|
return fmt.Errorf("%w: rolle %q hat kein recht %q", ErrDenied, role, perm)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Guard ist die zentrale Enforcement-Funktion (Akzeptanzkriterium 1): query
|
|
// wird NUR aufgerufen, wenn Authorize zustimmt. Es gibt keinen Weg, query
|
|
// ausserhalb von Guard aufzurufen und trotzdem den Aufrufer als autorisiert
|
|
// zu behandeln — die Autorisierungsentscheidung steht immer VOR dem
|
|
// Datenzugriff, nie danach.
|
|
func Guard[T any](ctx context.Context, e *Enforcer, role rbac.Role, perm rbac.Permission, query func(ctx context.Context) (T, error)) (T, error) {
|
|
var zero T
|
|
if err := e.Authorize(ctx, role, perm); err != nil {
|
|
return zero, err
|
|
}
|
|
return query(ctx)
|
|
}
|
|
|
|
// GuardTenantScoped erzwingt zusaetzlich, dass tenantSlug TEIL der Query-
|
|
// Funktion selbst ist (Akzeptanzkriterium 3): der Funktionstyp verlangt,
|
|
// dass die Repository-Implementierung tenantSlug in ihre eigene WHERE-
|
|
// Klausel einbaut — ein nachgelagerter Filter auf dem Ergebnis (der
|
|
// archivmail-Fehler aus "Bekannte Fehler vermeiden") ist mit dieser
|
|
// Signatur nicht moeglich, da die Query-Funktion tenantSlug selbst
|
|
// entgegennimmt und dafuer verantwortlich ist, statt ihn hinterher
|
|
// anzuwenden.
|
|
func GuardTenantScoped[T any](ctx context.Context, e *Enforcer, role rbac.Role, perm rbac.Permission, tenantSlug string, query func(ctx context.Context, tenantSlug string) (T, error)) (T, error) {
|
|
var zero T
|
|
if err := e.Authorize(ctx, role, perm); err != nil {
|
|
return zero, err
|
|
}
|
|
return query(ctx, tenantSlug)
|
|
}
|