package policy import ( "context" "errors" "os" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/flag" "gitea.perlbach24.de/scripte/nexarch/internal/rbac" ) func setupModuleScopeTest(t *testing.T) (*Store, *Enforcer, *flag.Store, *flag.Service, func()) { t.Helper() adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("pool: %v", err) } if _, err := pool.Exec(ctx, ` CREATE TABLE IF NOT EXISTS policy_rules ( role TEXT NOT NULL, permission TEXT NOT NULL, granted_by TEXT NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (role, permission) ); CREATE TABLE IF NOT EXISTS policy_rule_changes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role TEXT NOT NULL, permission TEXT NOT NULL, action TEXT NOT NULL CHECK (action IN ('grant','revoke')), actor TEXT NOT NULL, version INT NOT NULL, changed_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS policy_module_scopes ( role TEXT NOT NULL, permission TEXT NOT NULL, module TEXT NOT NULL, flag_key TEXT NOT NULL, PRIMARY KEY (role, permission) ); CREATE TABLE IF NOT EXISTS feature_flags ( key TEXT PRIMARY KEY, enabled BOOLEAN NOT NULL DEFAULT false, rollout_percentage INT NOT NULL DEFAULT 0, target_tenant_slugs TEXT[] NOT NULL DEFAULT '{}', updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); `); err != nil { t.Fatalf("schema: %v", err) } store := NewStore(pool) flagStore := flag.NewStore(pool) flagService := flag.NewService(flagStore, 10*time.Millisecond) // kurze TTL fuer testbare invalidierung cleanup := func() { _, _ = pool.Exec(ctx, `DELETE FROM policy_module_scopes WHERE role LIKE 'test\_%' ESCAPE '\'`) _, _ = pool.Exec(ctx, `DELETE FROM policy_rule_changes WHERE role LIKE 'test\_%' ESCAPE '\'`) _, _ = pool.Exec(ctx, `DELETE FROM policy_rules WHERE role LIKE 'test\_%' ESCAPE '\'`) _, _ = pool.Exec(ctx, `DELETE FROM feature_flags WHERE key LIKE 'test\_%' ESCAPE '\'`) pool.Close() } return store, NewEnforcer(store), flagStore, flagService, cleanup } // Akzeptanzkriterium 1 + Pruefung 1: Berechtigung fuer nicht aktiviertes // Modul greift nicht, selbst bei sonst passender Rolle. func TestAuthorizeForTenant_DeniesWhenModuleNotActivated(t *testing.T) { store, enforcer, _, flagService, cleanup := setupModuleScopeTest(t) defer cleanup() ctx := context.Background() role := rbac.Role("test_dms_nutzer") perm := rbac.Permission("test_dokumente_lesen") if err := store.Grant(ctx, role, perm, "admin@example.com"); err != nil { t.Fatalf("grant: %v", err) } if err := store.SetModuleScope(ctx, role, perm, "dms", "test_dms_enabled"); err != nil { t.Fatalf("set module scope: %v", err) } // Flag existiert nicht/ist nicht gesetzt -> IsEnabled liefert false (Fail-Safe-Default). queryCalled := false _, err := GuardModuleScoped(ctx, enforcer, flagService, "acme", role, perm, func(ctx context.Context) (string, error) { queryCalled = true return "daten", nil }) if !errors.Is(err, ErrDenied) { t.Fatalf("erwartet ErrDenied bei deaktiviertem modul, habe %v", err) } if queryCalled { t.Fatal("query haette bei deaktiviertem modul nicht aufgerufen werden duerfen") } } // Akzeptanzkriterium 2 + Pruefung 2: Aktivierung des Moduls macht die // Berechtigung ohne Neustart wirksam. func TestAuthorizeForTenant_BecomesActiveWithoutRestart(t *testing.T) { store, enforcer, flagStore, flagService, cleanup := setupModuleScopeTest(t) defer cleanup() ctx := context.Background() role := rbac.Role("test_dms_nutzer2") perm := rbac.Permission("test_dokumente_schreiben") if err := store.Grant(ctx, role, perm, "admin@example.com"); err != nil { t.Fatalf("grant: %v", err) } if err := store.SetModuleScope(ctx, role, perm, "dms", "test_dms_enabled2"); err != nil { t.Fatalf("set module scope: %v", err) } if err := enforcer.AuthorizeForTenant(ctx, flagService, "acme", role, perm); !errors.Is(err, ErrDenied) { t.Fatalf("vor aktivierung: erwartet ErrDenied, habe %v", err) } // Modul "im laufenden Betrieb" aktivieren — derselbe Prozess, kein Neustart. if err := flagStore.Set(ctx, flag.Flag{Key: "test_dms_enabled2", Enabled: true}); err != nil { t.Fatalf("flag setzen: %v", err) } time.Sleep(20 * time.Millisecond) // TTL abwarten statt Neustart if err := enforcer.AuthorizeForTenant(ctx, flagService, "acme", role, perm); err != nil { t.Fatalf("nach aktivierung sollte erlaubt sein: %v", err) } } // Akzeptanzkriterium 3 + Pruefung 3: Zusammenspiel Modul-Scope + Rollenscope // in Kombinationsfaellen. func TestAuthorizeForTenant_CombinationsOfRoleAndModuleScope(t *testing.T) { store, enforcer, flagStore, flagService, cleanup := setupModuleScopeTest(t) defer cleanup() ctx := context.Background() scopedRole := rbac.Role("test_scoped_rolle") unscopedRole := rbac.Role("test_unscoped_rolle") perm := rbac.Permission("test_kombiniert") // Fall 1: Rolle ohne jegliche Regel -> verboten, unabhaengig vom Flag. if err := enforcer.AuthorizeForTenant(ctx, flagService, "acme", rbac.Role("test_unbekannt"), perm); !errors.Is(err, ErrDenied) { t.Fatalf("fall 1: erwartet ErrDenied (keine regel), habe %v", err) } // Fall 2: Regel vorhanden, KEIN Modul-Scope -> immer erlaubt (Grundrecht ohne Lizenzbindung). if err := store.Grant(ctx, unscopedRole, perm, "admin@example.com"); err != nil { t.Fatalf("grant unscoped: %v", err) } if err := enforcer.AuthorizeForTenant(ctx, flagService, "acme", unscopedRole, perm); err != nil { t.Fatalf("fall 2: erwartet erlaubt ohne modul-scope, habe %v", err) } // Fall 3: Regel + Modul-Scope, Flag aus -> verboten. if err := store.Grant(ctx, scopedRole, perm, "admin@example.com"); err != nil { t.Fatalf("grant scoped: %v", err) } if err := store.SetModuleScope(ctx, scopedRole, perm, "mail", "test_mail_enabled"); err != nil { t.Fatalf("set module scope: %v", err) } if err := enforcer.AuthorizeForTenant(ctx, flagService, "acme", scopedRole, perm); !errors.Is(err, ErrDenied) { t.Fatalf("fall 3: erwartet ErrDenied (modul aus), habe %v", err) } // Fall 4: Regel + Modul-Scope, Flag an -> erlaubt. if err := flagStore.Set(ctx, flag.Flag{Key: "test_mail_enabled", Enabled: true}); err != nil { t.Fatalf("flag setzen: %v", err) } time.Sleep(20 * time.Millisecond) if err := enforcer.AuthorizeForTenant(ctx, flagService, "acme", scopedRole, perm); err != nil { t.Fatalf("fall 4: erwartet erlaubt (modul an), habe %v", err) } }