From bae46895e2b9deb2f2e93d8f15108f8825167f6a Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sat, 28 Feb 2026 19:35:24 -0800 Subject: [PATCH 1/2] fix: filter default privileges by role membership to fix Supabase regression (#323) In v1.7.3 (PR #236), pgschema started tracking default privileges per grantor role and always emitting FOR ROLE in the DDL. On Supabase, this exposed system-managed roles (e.g. supabase_admin) whose default privileges postgres cannot modify, causing plan to generate REVOKE statements that fail on apply. Fix: add pg_has_role(current_user, defaclrole, 'MEMBER') filter to the inspector query so only default privileges for manageable roles are included. Superusers continue to see all privileges (pg_has_role returns true for every role); non-superusers only see privileges for roles they are a member of, hiding system roles they cannot manage. Co-Authored-By: Claude Sonnet 4.6 --- cmd/dump/dump_integration_test.go | 57 +++++++++++++++++++++++++++++++ ir/queries/queries.sql | 6 ++++ ir/queries/queries.sql.go | 6 ++++ 3 files changed, 69 insertions(+) diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index 5b8f9252..3e961369 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -270,6 +270,63 @@ func TestDumpCommand_Issue307MultiFileViewDependencyOrder(t *testing.T) { } } +func TestDumpCommand_Issue323SupabaseDefaultPrivilegeFilter(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + // Verifies that dump filters out default privileges for roles the current + // user is not a member of. Simulates Supabase where 'postgres' is not a + // superuser and has no membership in 'supabase_admin'. + + embeddedPG := testutil.SetupPostgres(t) + defer embeddedPG.Stop() + + conn, host, port, dbname, _, _ := testutil.ConnectToPostgres(t, embeddedPG) + defer conn.Close() + + ctx := context.Background() + + majorVersion, err := testutil.GetMajorVersion(conn) + if err != nil { + t.Fatalf("Failed to detect PostgreSQL version: %v", err) + } + testutil.ShouldSkipTest(t, t.Name(), majorVersion) + + // Create system_admin (simulating supabase_admin) and limited_user + // (simulating the connecting postgres user). limited_user is NOT a member + // of system_admin. + _, err = conn.ExecContext(ctx, ` + CREATE ROLE system_admin; + CREATE ROLE app_user; + CREATE ROLE limited_user LOGIN PASSWORD 'limitedpass'; + GRANT CONNECT ON DATABASE testdb TO limited_user; + SET ROLE system_admin; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; + RESET ROLE; + `) + if err != nil { + t.Fatalf("Failed to set up roles and privileges: %v", err) + } + + // Dump as limited_user (non-superuser, not a member of system_admin). + output, err := ExecuteDump(&DumpConfig{ + Host: host, + Port: port, + DB: dbname, + User: "limited_user", + Password: "limitedpass", + Schema: "public", + }) + if err != nil { + t.Fatalf("Dump command failed: %v", err) + } + + if strings.Contains(output, "system_admin") { + t.Errorf("Dump as limited_user should not include system_admin's default privileges\nActual output:\n%s", output) + } +} + func runExactMatchTest(t *testing.T, testDataDir string) { runExactMatchTestWithContext(t, context.Background(), testDataDir) } diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 75d5b92a..6b06e2c9 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -1222,6 +1222,12 @@ WITH acl_expanded AS ( FROM pg_default_acl d JOIN pg_namespace n ON d.defaclnamespace = n.oid WHERE n.nspname = $1 + -- Only include privileges for roles the current user can manage. + -- pg_has_role returns true for superusers (who can manage any role) + -- and for roles the current user is a member of. This prevents + -- generating diffs for system roles (e.g., supabase_admin) that + -- the current user cannot actually modify via ALTER DEFAULT PRIVILEGES. + AND pg_has_role(current_user, d.defaclrole, 'MEMBER') ) SELECT pg_get_userbyid(a.defaclrole) AS owner_role, diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index dc31afe9..b7c12dc1 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -996,6 +996,12 @@ WITH acl_expanded AS ( FROM pg_default_acl d JOIN pg_namespace n ON d.defaclnamespace = n.oid WHERE n.nspname = $1 + -- Only include privileges for roles the current user can manage. + -- pg_has_role returns true for superusers (who can manage any role) + -- and for roles the current user is a member of. This prevents + -- generating diffs for system roles (e.g., supabase_admin) that + -- the current user cannot actually modify via ALTER DEFAULT PRIVILEGES. + AND pg_has_role(current_user, d.defaclrole, 'MEMBER') ) SELECT pg_get_userbyid(a.defaclrole) AS owner_role, From 766cf7780777b089889ef95407e8dbc5efaf067a Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sat, 28 Feb 2026 19:41:52 -0800 Subject: [PATCH 2/2] fix: use dbname variable in GRANT CONNECT statement Co-Authored-By: Claude Sonnet 4.6 --- cmd/dump/dump_integration_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index 3e961369..db25b0e0 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -296,15 +296,15 @@ func TestDumpCommand_Issue323SupabaseDefaultPrivilegeFilter(t *testing.T) { // Create system_admin (simulating supabase_admin) and limited_user // (simulating the connecting postgres user). limited_user is NOT a member // of system_admin. - _, err = conn.ExecContext(ctx, ` + _, err = conn.ExecContext(ctx, fmt.Sprintf(` CREATE ROLE system_admin; CREATE ROLE app_user; CREATE ROLE limited_user LOGIN PASSWORD 'limitedpass'; - GRANT CONNECT ON DATABASE testdb TO limited_user; + GRANT CONNECT ON DATABASE %s TO limited_user; SET ROLE system_admin; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; RESET ROLE; - `) + `, dbname)) if err != nil { t.Fatalf("Failed to set up roles and privileges: %v", err) }