admin → RBAC group migration: grant-based access replaces role check
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 19s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Successful in 2m14s
CI/CD / test-sqlite (pull_request) Successful in 2m56s
CI/CD / build-and-deploy (pull_request) Successful in 1m21s

surface.admin.access permission + seeded Admins system group replaces
hardcoded role == "admin" middleware checks. Admin bypass removed from
RequirePermission — all permissions flow through group membership.
Bootstrap, seed, OIDC, and admin handlers sync group membership on
role changes. Demotion/deletion safeguards use group member count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 16:14:46 +00:00
parent b9180d4184
commit 316c9cbfa7
15 changed files with 208 additions and 50 deletions

View File

@@ -304,6 +304,31 @@ func TruncateAll(t *testing.T) {
}
}
// Re-seed system groups (truncated above).
if IsSQLite() {
DB.Exec(`INSERT OR IGNORE INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES ('00000000-0000-0000-0000-000000000001', 'Everyone',
'Implicit group — all authenticated users receive these permissions.',
'global', NULL, 'system', '["extension.use","workflow.submit"]')`)
DB.Exec(`INSERT OR IGNORE INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES ('00000000-0000-0000-0000-000000000002', 'Admins',
'Full platform access — replaces legacy admin role.',
'global', NULL, 'system',
'["surface.admin.access","extension.use","extension.install","workflow.create","workflow.submit","admin.view","token.unlimited"]')`)
} else {
DB.Exec(`INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES ('00000000-0000-0000-0000-000000000001', 'Everyone',
'Implicit group — all authenticated users receive these permissions.',
'global', NULL, 'system', '["extension.use","workflow.submit"]'::jsonb)
ON CONFLICT (id) DO NOTHING`)
DB.Exec(`INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES ('00000000-0000-0000-0000-000000000002', 'Admins',
'Full platform access — replaces legacy admin role.',
'global', NULL, 'system',
'["surface.admin.access","extension.use","extension.install","workflow.create","workflow.submit","admin.view","token.unlimited"]'::jsonb)
ON CONFLICT (id) DO NOTHING`)
}
// Re-seed default config rows.
if IsSQLite() {
DB.Exec(`
@@ -377,6 +402,27 @@ func SeedTestUser(t *testing.T, username, email string) string {
return id
}
// SeedAdminsGroupMember adds a user to the Admins system group in test databases.
// Call after creating a user with role='admin' so they receive surface.admin.access
// through the normal permission resolution path.
func SeedAdminsGroupMember(t *testing.T, userID string) {
t.Helper()
const adminsGroupID = "00000000-0000-0000-0000-000000000002"
if IsSQLite() {
_, err := DB.Exec(`INSERT OR IGNORE INTO group_members (id, group_id, user_id, added_by) VALUES (?, ?, ?, ?)`,
uuid.New().String(), adminsGroupID, userID, userID)
if err != nil {
t.Fatalf("SeedAdminsGroupMember: %v", err)
}
return
}
_, err := DB.Exec(`INSERT INTO group_members (group_id, user_id, added_by) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
adminsGroupID, userID, userID)
if err != nil {
t.Fatalf("SeedAdminsGroupMember: %v", err)
}
}
// SeedTestChannel creates a test channel owned by userID and returns the channel ID.
func SeedTestChannel(t *testing.T, userID, title string) string {
t.Helper()