internal/policy/module_scope.go: policy_module_scopes verknuepft optional eine (role, permission)-Regel mit einem LIC-02-Feature-Flag. Enforcer. AuthorizeForTenant ist DIESELBE zentrale Entscheidungsfunktion wie Authorize (Akzeptanzkriterium 3, kein zweiter Enforcement-Mechanismus) — prueft zusaetzlich zur Grundregel, ob das verknuepfte Modul fuer den Tenant aktiv ist. Existiert kein ModuleScope-Eintrag, bleibt eine Regel wie bisher ohne Lizenzbindung gueltig (Kombinationsfall). GuardModuleScoped erweitert policy.Guard um dieselbe Pruefung. internal/flag (LIC-02) wurde 1:1 aus dem lic-02-Branch uebernommen (git show aus derselben Repo-Historie, keine Aenderung) — RBAC-04 haengt sowohl an RBAC-01/02 als auch an LIC-02, aber diese leben auf getrennten, noch nicht gemergten Feature-Branches ohne gemeinsame Historie. Fail-Safe-Verhalten aus LIC-02 greift automatisch: ein nicht konfiguriertes oder nicht erreichbares Feature-Flag gilt als deaktiviert, nie als aktiviert (sicherer Default fuer Modul-Aktivierungspruefungen). Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Zugriff auf deaktiviertes Modul trotz passender Rolle abgewiesen — TestAuthorizeForTenant_DeniesWhenModuleNotActivated: GuardModuleScoped ruft die Query-Funktion nachweislich nicht auf. PASS. 2. Reaktivierung macht Berechtigung im laufenden Betrieb wirksam, kein Neustart — TestAuthorizeForTenant_BecomesActiveWithoutRestart: derselbe Enforcer/Service-Prozess, Flag per Store.Set aktiviert, TTL abgewartet, danach erlaubt. PASS. 3. Zusammenspiel Modul-Scope + Rollenscope in Kombinationsfaellen — TestAuthorizeForTenant_CombinationsOfRoleAndModuleScope: keine Regel -> verboten; Regel ohne Modul-Scope -> immer erlaubt; Regel mit Modul-Scope und Flag aus -> verboten; Flag an -> erlaubt. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
91 lines
3.3 KiB
Go
91 lines
3.3 KiB
Go
package policy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
|
|
)
|
|
|
|
// ModuleScope verknuepft eine Policy-Regel mit einem Feature-Flag: existiert
|
|
// ein ModuleScope fuer (role, permission), gilt die Regel nur zusaetzlich zur
|
|
// Grundberechtigung, wenn FlagKey fuer den jeweiligen Tenant aktiv ist
|
|
// (Akzeptanzkriterium 1: Rechte folgen der Lizenz).
|
|
type ModuleScope struct {
|
|
Role rbac.Role
|
|
Permission rbac.Permission
|
|
Module string
|
|
FlagKey string
|
|
}
|
|
|
|
// SetModuleScope verknuepft eine bestehende Policy-Regel mit einem Modul/
|
|
// Feature-Flag. Die Regel selbst (Store.Grant) muss unabhaengig davon
|
|
// existieren — ModuleScope schraenkt sie nur zusaetzlich ein.
|
|
func (s *Store) SetModuleScope(ctx context.Context, role rbac.Role, perm rbac.Permission, module, flagKey string) error {
|
|
_, err := s.pool.Exec(ctx, `
|
|
INSERT INTO policy_module_scopes (role, permission, module, flag_key)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (role, permission) DO UPDATE SET module = $3, flag_key = $4
|
|
`, string(role), string(perm), module, flagKey)
|
|
if err != nil {
|
|
return fmt.Errorf("modul-scope setzen: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) GetModuleScope(ctx context.Context, role rbac.Role, perm rbac.Permission) (ModuleScope, bool, error) {
|
|
var ms ModuleScope
|
|
ms.Role, ms.Permission = role, perm
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT module, flag_key FROM policy_module_scopes WHERE role = $1 AND permission = $2
|
|
`, string(role), string(perm)).Scan(&ms.Module, &ms.FlagKey)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ModuleScope{}, false, nil
|
|
}
|
|
return ModuleScope{}, false, fmt.Errorf("modul-scope lesen: %w", err)
|
|
}
|
|
return ms, true, nil
|
|
}
|
|
|
|
// AuthorizeForTenant ist dieselbe zentrale Entscheidungsfunktion wie
|
|
// Authorize (Akzeptanzkriterium 3: keine zweite Enforcement-Schicht),
|
|
// erweitert um die Modul-Scoping-Pruefung: eine sonst passende Regel greift
|
|
// NICHT, wenn das zugehoerige Modul fuer den Tenant nicht aktiviert ist
|
|
// (Akzeptanzkriterium 1). Feature-Flag-Aenderungen wirken ohne Neustart
|
|
// (Akzeptanzkriterium 2), da flag.Service dieselbe TTL-Cache-Instanz der
|
|
// aufrufenden Core-Instanz nutzt.
|
|
func (e *Enforcer) AuthorizeForTenant(ctx context.Context, flags *flag.Service, tenantSlug string, role rbac.Role, perm rbac.Permission) error {
|
|
if err := e.Authorize(ctx, role, perm); err != nil {
|
|
return err
|
|
}
|
|
|
|
scope, found, err := e.store.GetModuleScope(ctx, role, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return nil // keine Modul-Bindung fuer diese Regel — Grundberechtigung reicht.
|
|
}
|
|
|
|
if !flags.IsEnabled(ctx, tenantSlug, scope.FlagKey) {
|
|
return fmt.Errorf("%w: modul %q ist fuer diesen mandanten nicht aktiviert", ErrDenied, scope.Module)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GuardModuleScoped ist Guard mit zusaetzlicher Modul-Scoping-Pruefung —
|
|
// dieselbe zentrale Enforcement-Funktion, kein paralleler Mechanismus
|
|
// (Akzeptanzkriterium 3).
|
|
func GuardModuleScoped[T any](ctx context.Context, e *Enforcer, flags *flag.Service, tenantSlug string, role rbac.Role, perm rbac.Permission, query func(ctx context.Context) (T, error)) (T, error) {
|
|
var zero T
|
|
if err := e.AuthorizeForTenant(ctx, flags, tenantSlug, role, perm); err != nil {
|
|
return zero, err
|
|
}
|
|
return query(ctx)
|
|
}
|