Skip to content

Commit ac164cc

Browse files
tianzhouclaude
andcommitted
feat: add support for privileges and default_privileges in .pgschemaignore (#339)
Add [privileges] and [default_privileges] sections to .pgschemaignore that filter grants by grantee role name patterns. This allows ignoring privilege statements for roles that don't exist in the plan database (e.g., production roles not available in embedded postgres). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8eeb00c commit ac164cc

4 files changed

Lines changed: 242 additions & 18 deletions

File tree

cmd/ignore_integration_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,181 @@ func verifyDumpOutput(t *testing.T, output string) {
951951
}
952952
}
953953

954+
func TestIgnorePrivileges(t *testing.T) {
955+
if testing.Short() {
956+
t.Skip("Skipping integration test in short mode")
957+
}
958+
959+
embeddedPG := testutil.SetupPostgres(t)
960+
defer embeddedPG.Stop()
961+
conn, host, port, dbname, user, password := testutil.ConnectToPostgres(t, embeddedPG)
962+
defer conn.Close()
963+
964+
containerInfo := &struct {
965+
Conn *sql.DB
966+
Host string
967+
Port int
968+
DBName string
969+
User string
970+
Password string
971+
}{
972+
Conn: conn,
973+
Host: host,
974+
Port: port,
975+
DBName: dbname,
976+
User: user,
977+
Password: password,
978+
}
979+
980+
// Create schema with roles and privileges
981+
setupSQL := `
982+
CREATE TABLE users (
983+
id SERIAL PRIMARY KEY,
984+
name TEXT NOT NULL,
985+
email TEXT NOT NULL
986+
);
987+
988+
CREATE TABLE orders (
989+
id SERIAL PRIMARY KEY,
990+
user_id INTEGER REFERENCES users(id),
991+
total_amount DECIMAL(10,2) NOT NULL
992+
);
993+
994+
DO $$
995+
BEGIN
996+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_reader') THEN
997+
CREATE ROLE app_reader;
998+
END IF;
999+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'deploy_bot') THEN
1000+
CREATE ROLE deploy_bot;
1001+
END IF;
1002+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'admin_role') THEN
1003+
CREATE ROLE admin_role;
1004+
END IF;
1005+
END $$;
1006+
1007+
-- Privileges to keep
1008+
GRANT SELECT ON users TO app_reader;
1009+
GRANT SELECT ON orders TO app_reader;
1010+
1011+
-- Privileges to ignore (deploy_bot)
1012+
GRANT ALL ON users TO deploy_bot;
1013+
GRANT ALL ON orders TO deploy_bot;
1014+
1015+
-- Privileges to ignore (admin_role)
1016+
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO admin_role;
1017+
1018+
-- Default privileges to keep
1019+
ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app_reader;
1020+
1021+
-- Default privileges to ignore (deploy_bot)
1022+
ALTER DEFAULT PRIVILEGES GRANT ALL ON TABLES TO deploy_bot;
1023+
`
1024+
_, err := conn.Exec(setupSQL)
1025+
if err != nil {
1026+
t.Fatalf("Failed to create test schema: %v", err)
1027+
}
1028+
1029+
originalWd, err := os.Getwd()
1030+
if err != nil {
1031+
t.Fatalf("Failed to get current working directory: %v", err)
1032+
}
1033+
defer os.Chdir(originalWd)
1034+
1035+
tmpDir := t.TempDir()
1036+
os.Chdir(tmpDir)
1037+
1038+
// Create .pgschemaignore with privileges section
1039+
ignoreContent := `[privileges]
1040+
patterns = ["deploy_bot", "admin_*"]
1041+
1042+
[default_privileges]
1043+
patterns = ["deploy_bot"]
1044+
`
1045+
err = os.WriteFile(".pgschemaignore", []byte(ignoreContent), 0644)
1046+
if err != nil {
1047+
t.Fatalf("Failed to create .pgschemaignore: %v", err)
1048+
}
1049+
1050+
t.Run("dump", func(t *testing.T) {
1051+
output := executeIgnoreDumpCommand(t, containerInfo)
1052+
1053+
// Privileges for app_reader should be present
1054+
if !strings.Contains(output, "app_reader") {
1055+
t.Error("Dump should include privileges for app_reader")
1056+
}
1057+
1058+
// Privileges for deploy_bot should be ignored
1059+
if strings.Contains(output, "deploy_bot") {
1060+
t.Error("Dump should not include privileges for deploy_bot (ignored)")
1061+
}
1062+
1063+
// Privileges for admin_role should be ignored (matches admin_*)
1064+
if strings.Contains(output, "admin_role") {
1065+
t.Error("Dump should not include privileges for admin_role (ignored by admin_* pattern)")
1066+
}
1067+
})
1068+
1069+
t.Run("plan", func(t *testing.T) {
1070+
// Create schema file that adds new privileges
1071+
schemaSQL := `
1072+
CREATE TABLE users (
1073+
id SERIAL PRIMARY KEY,
1074+
name TEXT NOT NULL,
1075+
email TEXT NOT NULL
1076+
);
1077+
1078+
CREATE TABLE orders (
1079+
id SERIAL PRIMARY KEY,
1080+
user_id INTEGER REFERENCES users(id),
1081+
total_amount DECIMAL(10,2) NOT NULL
1082+
);
1083+
1084+
DO $$
1085+
BEGIN
1086+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_reader') THEN
1087+
CREATE ROLE app_reader;
1088+
END IF;
1089+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'deploy_bot') THEN
1090+
CREATE ROLE deploy_bot;
1091+
END IF;
1092+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'admin_role') THEN
1093+
CREATE ROLE admin_role;
1094+
END IF;
1095+
END $$;
1096+
1097+
-- Keep these privileges
1098+
GRANT SELECT ON users TO app_reader;
1099+
GRANT SELECT ON orders TO app_reader;
1100+
1101+
-- These should be ignored in plan
1102+
GRANT ALL ON users TO deploy_bot;
1103+
GRANT ALL ON orders TO deploy_bot;
1104+
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO admin_role;
1105+
1106+
-- Default privileges
1107+
ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app_reader;
1108+
ALTER DEFAULT PRIVILEGES GRANT ALL ON TABLES TO deploy_bot;
1109+
`
1110+
schemaFile := "schema_privs.sql"
1111+
err := os.WriteFile(schemaFile, []byte(schemaSQL), 0644)
1112+
if err != nil {
1113+
t.Fatalf("Failed to create schema file: %v", err)
1114+
}
1115+
defer os.Remove(schemaFile)
1116+
1117+
output := executeIgnorePlanCommand(t, containerInfo, schemaFile)
1118+
1119+
// Plan should not contain any changes for ignored roles
1120+
if strings.Contains(output, "deploy_bot") {
1121+
t.Error("Plan should not include changes for deploy_bot (ignored)")
1122+
}
1123+
if strings.Contains(output, "admin_role") {
1124+
t.Error("Plan should not include changes for admin_role (ignored)")
1125+
}
1126+
})
1127+
}
1128+
9541129
// verifyPlanOutput checks that plan output excludes ignored objects
9551130
func verifyPlanOutput(t *testing.T, output string) {
9561131
// Changes that should appear in plan (regular objects)

cmd/util/ignoreloader.go

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ func LoadIgnoreFileFromPath(filePath string) (*ir.IgnoreConfig, error) {
2828
// TomlConfig represents the TOML structure of the .pgschemaignore file
2929
// This is used for parsing more complex configurations if needed in the future
3030
type TomlConfig struct {
31-
Tables TableIgnoreConfig `toml:"tables,omitempty"`
32-
Views ViewIgnoreConfig `toml:"views,omitempty"`
33-
Functions FunctionIgnoreConfig `toml:"functions,omitempty"`
34-
Procedures ProcedureIgnoreConfig `toml:"procedures,omitempty"`
35-
Types TypeIgnoreConfig `toml:"types,omitempty"`
36-
Sequences SequenceIgnoreConfig `toml:"sequences,omitempty"`
31+
Tables TableIgnoreConfig `toml:"tables,omitempty"`
32+
Views ViewIgnoreConfig `toml:"views,omitempty"`
33+
Functions FunctionIgnoreConfig `toml:"functions,omitempty"`
34+
Procedures ProcedureIgnoreConfig `toml:"procedures,omitempty"`
35+
Types TypeIgnoreConfig `toml:"types,omitempty"`
36+
Sequences SequenceIgnoreConfig `toml:"sequences,omitempty"`
37+
Privileges PrivilegeIgnoreConfig `toml:"privileges,omitempty"`
38+
DefaultPrivileges DefaultPrivilegeIgnoreConfig `toml:"default_privileges,omitempty"`
3739
}
3840

3941
// TableIgnoreConfig represents table-specific ignore configuration
@@ -66,6 +68,18 @@ type SequenceIgnoreConfig struct {
6668
Patterns []string `toml:"patterns,omitempty"`
6769
}
6870

71+
// PrivilegeIgnoreConfig represents privilege-specific ignore configuration
72+
// Patterns match on grantee role names
73+
type PrivilegeIgnoreConfig struct {
74+
Patterns []string `toml:"patterns,omitempty"`
75+
}
76+
77+
// DefaultPrivilegeIgnoreConfig represents default privilege-specific ignore configuration
78+
// Patterns match on grantee role names
79+
type DefaultPrivilegeIgnoreConfig struct {
80+
Patterns []string `toml:"patterns,omitempty"`
81+
}
82+
6983
// LoadIgnoreFileWithStructure loads the .pgschemaignore file using the structured TOML format
7084
// and converts it to the simple IgnoreConfig structure
7185
func LoadIgnoreFileWithStructure() (*ir.IgnoreConfig, error) {
@@ -91,12 +105,14 @@ func LoadIgnoreFileWithStructureFromPath(filePath string) (*ir.IgnoreConfig, err
91105

92106
// Convert to simple IgnoreConfig structure
93107
config := &ir.IgnoreConfig{
94-
Tables: tomlConfig.Tables.Patterns,
95-
Views: tomlConfig.Views.Patterns,
96-
Functions: tomlConfig.Functions.Patterns,
97-
Procedures: tomlConfig.Procedures.Patterns,
98-
Types: tomlConfig.Types.Patterns,
99-
Sequences: tomlConfig.Sequences.Patterns,
108+
Tables: tomlConfig.Tables.Patterns,
109+
Views: tomlConfig.Views.Patterns,
110+
Functions: tomlConfig.Functions.Patterns,
111+
Procedures: tomlConfig.Procedures.Patterns,
112+
Types: tomlConfig.Types.Patterns,
113+
Sequences: tomlConfig.Sequences.Patterns,
114+
Privileges: tomlConfig.Privileges.Patterns,
115+
DefaultPrivileges: tomlConfig.DefaultPrivileges.Patterns,
100116
}
101117

102118
return config, nil

ir/ignore.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77

88
// IgnoreConfig represents the configuration for ignoring database objects
99
type IgnoreConfig struct {
10-
Tables []string `toml:"tables,omitempty"`
11-
Views []string `toml:"views,omitempty"`
12-
Functions []string `toml:"functions,omitempty"`
13-
Procedures []string `toml:"procedures,omitempty"`
14-
Types []string `toml:"types,omitempty"`
15-
Sequences []string `toml:"sequences,omitempty"`
10+
Tables []string `toml:"tables,omitempty"`
11+
Views []string `toml:"views,omitempty"`
12+
Functions []string `toml:"functions,omitempty"`
13+
Procedures []string `toml:"procedures,omitempty"`
14+
Types []string `toml:"types,omitempty"`
15+
Sequences []string `toml:"sequences,omitempty"`
16+
Privileges []string `toml:"privileges,omitempty"`
17+
DefaultPrivileges []string `toml:"default_privileges,omitempty"`
1618
}
1719

1820
// ShouldIgnoreTable checks if a table should be ignored based on the patterns
@@ -63,6 +65,22 @@ func (c *IgnoreConfig) ShouldIgnoreSequence(sequenceName string) bool {
6365
return c.shouldIgnore(sequenceName, c.Sequences)
6466
}
6567

68+
// ShouldIgnorePrivilege checks if a privilege should be ignored based on the grantee role name
69+
func (c *IgnoreConfig) ShouldIgnorePrivilege(grantee string) bool {
70+
if c == nil {
71+
return false
72+
}
73+
return c.shouldIgnore(grantee, c.Privileges)
74+
}
75+
76+
// ShouldIgnoreDefaultPrivilege checks if a default privilege should be ignored based on the grantee role name
77+
func (c *IgnoreConfig) ShouldIgnoreDefaultPrivilege(grantee string) bool {
78+
if c == nil {
79+
return false
80+
}
81+
return c.shouldIgnore(grantee, c.DefaultPrivileges)
82+
}
83+
6684
// shouldIgnore checks if a name should be ignored based on the patterns
6785
// Patterns support wildcards (*) and negation (!)
6886
// Negation patterns (starting with !) take precedence over inclusion patterns

ir/inspector.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,6 +2030,11 @@ func (i *Inspector) buildPrivileges(ctx context.Context, schema *IR, targetSchem
20302030
continue
20312031
}
20322032

2033+
// Skip privileges for ignored grantees
2034+
if i.ignoreConfig != nil && i.ignoreConfig.ShouldIgnorePrivilege(grantee) {
2035+
continue
2036+
}
2037+
20332038
// Check for default PUBLIC grants that should be excluded
20342039
if grantee == "PUBLIC" {
20352040
if (objectType == "FUNCTION" || objectType == "PROCEDURE") && privilegeType == "EXECUTE" {
@@ -2174,6 +2179,11 @@ func (i *Inspector) buildDefaultPrivileges(ctx context.Context, schema *IR, targ
21742179
continue
21752180
}
21762181

2182+
// Skip default privileges for ignored grantees
2183+
if i.ignoreConfig != nil && i.ignoreConfig.ShouldIgnoreDefaultPrivilege(p.Grantee.String) {
2184+
continue
2185+
}
2186+
21772187
key := privKey{
21782188
OwnerRole: p.OwnerRole.String,
21792189
ObjectType: p.ObjectType.String,
@@ -2369,6 +2379,11 @@ func (i *Inspector) buildColumnPrivileges(ctx context.Context, schema *IR, targe
23692379
}
23702380
}
23712381

2382+
// Skip column privileges for ignored grantees
2383+
if i.ignoreConfig != nil && i.ignoreConfig.ShouldIgnorePrivilege(grantee) {
2384+
continue
2385+
}
2386+
23722387
key := colPrivKey{
23732388
TableName: tableName,
23742389
Grantee: grantee,

0 commit comments

Comments
 (0)