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) }