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