package rbac import ( "context" "fmt" "os" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/user" ) const groupSchema = ` CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS role_assignments ( user_id UUID PRIMARY KEY REFERENCES users(id), role TEXT NOT NULL CHECK (role IN ('user','tenant_admin')), granted_by TEXT NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, role TEXT CHECK (role IN ('user','tenant_admin')), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS group_members ( group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id), added_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (group_id, user_id) );` func setupGroupTestDB(t *testing.T, dbName string) *pgxpool.Pool { t.Helper() adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() adminPool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("admin pool: %v", err) } _, _ = adminPool.Exec(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %q`, dbName)) if _, err := adminPool.Exec(ctx, fmt.Sprintf(`CREATE DATABASE %q`, dbName)); err != nil { t.Fatalf("testdatenbank anlegen: %v", err) } dsn := strings.Replace(adminDSN, "/postgres?", "/"+dbName+"?", 1) pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("connect testdatenbank: %v", err) } if _, err := pool.Exec(ctx, groupSchema); err != nil { t.Fatalf("schema anwenden: %v", err) } t.Cleanup(func() { pool.Close() _, _ = adminPool.Exec(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %q`, dbName)) adminPool.Close() }) return pool } func createTestUser(t *testing.T, pool *pgxpool.Pool, email string) string { t.Helper() u, err := user.NewTenantUserStore(pool).Create(context.Background(), email, "Test") if err != nil { t.Fatalf("testuser anlegen: %v", err) } return u.ID } // Akzeptanzkriterium 1: Gruppen anlegen und Benutzer zuordnen. func TestGroup_CreateAndAddMember(t *testing.T) { pool := setupGroupTestDB(t, "test_rbac03_create") gs := NewGroupStore(pool) ctx := context.Background() g, err := gs.CreateGroup(ctx, "Buchhaltung") if err != nil { t.Fatalf("create group: %v", err) } userID := createTestUser(t, pool, "alice@example.com") if err := gs.AddMember(ctx, g.ID, userID); err != nil { t.Fatalf("add member: %v", err) } } // Akzeptanzkriterium 2 + Pruefung 1: Gruppenrolle wirkt auf ALLE aktuellen // Mitglieder. func TestGroup_RoleAffectsAllCurrentMembers(t *testing.T) { pool := setupGroupTestDB(t, "test_rbac03_roleall") gs := NewGroupStore(pool) ctx := context.Background() g, err := gs.CreateGroup(ctx, "Admins") if err != nil { t.Fatalf("create group: %v", err) } if err := gs.SetGroupRole(ctx, g.ID, RoleTenantAdmin); err != nil { t.Fatalf("set group role: %v", err) } alice := createTestUser(t, pool, "alice@example.com") bob := createTestUser(t, pool, "bob@example.com") if err := gs.AddMember(ctx, g.ID, alice); err != nil { t.Fatalf("add alice: %v", err) } if err := gs.AddMember(ctx, g.ID, bob); err != nil { t.Fatalf("add bob: %v", err) } for _, uid := range []string{alice, bob} { perms, err := gs.EffectivePermissionsForUser(ctx, nil, uid) if err != nil { t.Fatalf("effective permissions fuer %s: %v", uid, err) } if !contains(perms, PermManageUsers) { t.Fatalf("mitglied %s sollte tenant_admin-rechte ueber die gruppe haben, habe %v", uid, perms) } } } // Akzeptanzkriterium 3 + Pruefung 2: Entfernen aus der Gruppe entzieht die // gruppenbasierten Rechte sofort. func TestGroup_RemoveMemberRevokesRightsImmediately(t *testing.T) { pool := setupGroupTestDB(t, "test_rbac03_remove") gs := NewGroupStore(pool) ctx := context.Background() g, _ := gs.CreateGroup(ctx, "Admins") _ = gs.SetGroupRole(ctx, g.ID, RoleTenantAdmin) userID := createTestUser(t, pool, "alice@example.com") _ = gs.AddMember(ctx, g.ID, userID) perms, err := gs.EffectivePermissionsForUser(ctx, nil, userID) if err != nil { t.Fatalf("effective permissions (mitglied): %v", err) } if !contains(perms, PermManageUsers) { t.Fatal("erwartet tenant_admin-rechte als mitglied") } if err := gs.RemoveMember(ctx, g.ID, userID); err != nil { t.Fatalf("remove member: %v", err) } perms, err = gs.EffectivePermissionsForUser(ctx, nil, userID) if err != nil { t.Fatalf("effective permissions (entfernt): %v", err) } if contains(perms, PermManageUsers) { t.Fatal("erwartet KEINE tenant_admin-rechte nach entfernen aus der gruppe") } } // Akzeptanzkriterium 3: Loeschen einer Gruppe entzieht Rechte sauber, ohne // das Benutzerkonto zu loeschen. func TestGroup_DeleteGroupRevokesRightsWithoutDeletingUser(t *testing.T) { pool := setupGroupTestDB(t, "test_rbac03_delete") gs := NewGroupStore(pool) us := user.NewTenantUserStore(pool) ctx := context.Background() g, _ := gs.CreateGroup(ctx, "Temp") _ = gs.SetGroupRole(ctx, g.ID, RoleTenantAdmin) userID := createTestUser(t, pool, "alice@example.com") _ = gs.AddMember(ctx, g.ID, userID) if err := gs.DeleteGroup(ctx, g.ID); err != nil { t.Fatalf("delete group: %v", err) } perms, err := gs.EffectivePermissionsForUser(ctx, nil, userID) if err != nil { t.Fatalf("effective permissions: %v", err) } if contains(perms, PermManageUsers) { t.Fatal("erwartet keine rechte mehr nach loeschen der gruppe") } if _, err := us.Get(ctx, userID); err != nil { t.Fatalf("benutzerkonto haette erhalten bleiben muessen: %v", err) } } // Akzeptanzkriterium 3 + Pruefung 3: Gruppen sind sauber tenant-isoliert. func TestGroup_TenantIsolation(t *testing.T) { poolA := setupGroupTestDB(t, "test_rbac03_tenanta") poolB := setupGroupTestDB(t, "test_rbac03_tenantb") gsA := NewGroupStore(poolA) ctx := context.Background() if _, err := gsA.CreateGroup(ctx, "Nur-Tenant-A"); err != nil { t.Fatalf("create group a: %v", err) } var countB int if err := poolB.QueryRow(ctx, `SELECT count(*) FROM groups`).Scan(&countB); err != nil { t.Fatalf("count tenant b: %v", err) } if countB != 0 { t.Fatalf("tenant b sollte keine gruppen aus tenant a sehen, habe %d", countB) } }