-
Notifications
You must be signed in to change notification settings - Fork 29
fix: skip revoking privileges covered by default privileges (#250) #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -294,3 +294,65 @@ func privilegesEqual(old, new *ir.Privilege) bool { | |
| func (d *privilegeDiff) GetObjectName() string { | ||
| return d.New.GetObjectKey() | ||
| } | ||
|
|
||
| // isPrivilegeCoveredByDefaultPrivileges checks if an explicit privilege is covered | ||
| // by default privileges in the desired state. This is used to avoid generating | ||
| // spurious REVOKE statements for privileges that are auto-granted via default privileges. | ||
| // See https://github.com/pgschema/pgschema/issues/250 | ||
| func isPrivilegeCoveredByDefaultPrivileges(p *ir.Privilege, defaultPrivileges []*ir.DefaultPrivilege) bool { | ||
| for _, dp := range defaultPrivileges { | ||
| // Match object types (TABLE -> TABLES, SEQUENCE -> SEQUENCES, etc.) | ||
| if !privilegeObjectTypeMatchesDefault(p.ObjectType, dp.ObjectType) { | ||
| continue | ||
| } | ||
|
|
||
| // Match grantee | ||
| if p.Grantee != dp.Grantee { | ||
| continue | ||
| } | ||
|
|
||
| // Match grant option | ||
| if p.WithGrantOption != dp.WithGrantOption { | ||
| continue | ||
| } | ||
|
|
||
| // Check if all privilege types are covered by the default privilege | ||
| if privilegesCoveredBy(p.Privileges, dp.Privileges) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
Comment on lines
+302
to
+325
|
||
|
|
||
| // privilegeObjectTypeMatchesDefault checks if a privilege object type matches | ||
| // a default privilege object type (e.g., TABLE matches TABLES) | ||
| func privilegeObjectTypeMatchesDefault(privType ir.PrivilegeObjectType, defaultType ir.DefaultPrivilegeObjectType) bool { | ||
| switch privType { | ||
| case ir.PrivilegeObjectTypeTable: | ||
| return defaultType == ir.DefaultPrivilegeObjectTypeTables | ||
| case ir.PrivilegeObjectTypeSequence: | ||
| return defaultType == ir.DefaultPrivilegeObjectTypeSequences | ||
| case ir.PrivilegeObjectTypeFunction: | ||
| return defaultType == ir.DefaultPrivilegeObjectTypeFunctions | ||
| case ir.PrivilegeObjectTypeProcedure: | ||
| return defaultType == ir.DefaultPrivilegeObjectTypeFunctions // Procedures use FUNCTIONS default | ||
| case ir.PrivilegeObjectTypeType: | ||
| return defaultType == ir.DefaultPrivilegeObjectTypeTypes | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
Comment on lines
+329
to
+344
|
||
|
|
||
| // privilegesCoveredBy checks if all privileges in 'privs' are covered by 'coveringPrivs' | ||
| func privilegesCoveredBy(privs, coveringPrivs []string) bool { | ||
| coveringSet := make(map[string]bool) | ||
| for _, p := range coveringPrivs { | ||
| coveringSet[p] = true | ||
| } | ||
| for _, p := range privs { | ||
| if !coveringSet[p] { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||||||||||
| -- https://github.com/pgschema/pgschema/issues/250 | ||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||
| -- Test: Privileges covered by default privileges should not be revoked. | ||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||
| -- This represents the desired state as written in the user's SQL files. | ||||||||||||||||||||||||||
| -- The user declares default privileges and creates objects, but does NOT | ||||||||||||||||||||||||||
| -- include explicit GRANTs because they expect PostgreSQL to auto-grant them. | ||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||
| -- The diff should NOT generate REVOKE statements because the privileges | ||||||||||||||||||||||||||
| -- in old.sql are covered by the default privileges defined here. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| DO $$ | ||||||||||||||||||||||||||
| BEGIN | ||||||||||||||||||||||||||
| IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN | ||||||||||||||||||||||||||
| CREATE ROLE app_role; | ||||||||||||||||||||||||||
| END IF; | ||||||||||||||||||||||||||
| IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'owner_role') THEN | ||||||||||||||||||||||||||
| CREATE ROLE owner_role; | ||||||||||||||||||||||||||
| END IF; | ||||||||||||||||||||||||||
| END $$; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| -- Default privileges for owner_role (same as old state) | ||||||||||||||||||||||||||
| ALTER DEFAULT PRIVILEGES FOR ROLE owner_role IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_role; | ||||||||||||||||||||||||||
| ALTER DEFAULT PRIVILEGES FOR ROLE owner_role IN SCHEMA public GRANT USAGE ON SEQUENCES TO app_role; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| -- Table and sequence (same as old state) | ||||||||||||||||||||||||||
| CREATE TABLE users ( | ||||||||||||||||||||||||||
| id serial PRIMARY KEY, | ||||||||||||||||||||||||||
| name text NOT NULL | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
Comment on lines
+26
to
+30
|
||||||||||||||||||||||||||
| -- Table and sequence (same as old state) | |
| CREATE TABLE users ( | |
| id serial PRIMARY KEY, | |
| name text NOT NULL | |
| ); | |
| -- Table and sequence (same as old state) | |
| SET ROLE owner_role; | |
| CREATE TABLE users ( | |
| id serial PRIMARY KEY, | |
| name text NOT NULL | |
| ); | |
| RESET ROLE; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| -- https://github.com/pgschema/pgschema/issues/250 | ||
| -- | ||
| -- Test: Privileges covered by default privileges should not be revoked. | ||
| -- | ||
| -- This test simulates the scenario where: | ||
| -- 1. Default privileges are configured for owner_role | ||
| -- 2. Objects were created by owner_role, so privileges were auto-granted | ||
| -- 3. The explicit GRANTs below represent those auto-granted privileges | ||
| -- | ||
| -- In a real database, these grants would be created automatically by PostgreSQL | ||
| -- when owner_role creates objects. Here we simulate this by adding explicit GRANTs. | ||
|
|
||
| DO $$ | ||
| BEGIN | ||
| IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN | ||
| CREATE ROLE app_role; | ||
| END IF; | ||
| IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'owner_role') THEN | ||
| CREATE ROLE owner_role; | ||
| END IF; | ||
| END $$; | ||
|
|
||
| -- Default privileges for owner_role | ||
| ALTER DEFAULT PRIVILEGES FOR ROLE owner_role IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_role; | ||
| ALTER DEFAULT PRIVILEGES FOR ROLE owner_role IN SCHEMA public GRANT USAGE ON SEQUENCES TO app_role; | ||
|
|
||
| -- Table and sequence | ||
| CREATE TABLE users ( | ||
| id serial PRIMARY KEY, | ||
| name text NOT NULL | ||
| ); | ||
|
|
||
| -- Simulate auto-granted privileges (what PostgreSQL would grant automatically | ||
| -- when owner_role creates objects with the above default privileges) | ||
| GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE users TO app_role; | ||
| GRANT USAGE ON SEQUENCE users_id_seq TO app_role; | ||
|
Comment on lines
+27
to
+36
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "version": "1.0.0", | ||
| "pgschema_version": "1.6.1", | ||
| "created_at": "1970-01-01T00:00:00Z", | ||
| "source_fingerprint": { | ||
| "hash": "16119adc01274eb2d4d4fdb177740d5176d82ff4330f7605e3c08a5926b8e734" | ||
| }, | ||
| "groups": null | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| No changes detected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code collects default privileges from all schemas in the new IR (lines 1110-1112) but doesn't check if the privilege's schema matches the default privilege's schema when calling isPrivilegeCoveredByDefaultPrivileges. This could lead to incorrect matches where a privilege in schema A is incorrectly considered covered by a default privilege in schema B.
The Privilege struct doesn't have a schema field, and the DefaultPrivilege struct also doesn't have a schema field. While privileges are queried per schema and stored in each schema's Privileges slice, when checking coverage, we need to ensure we're only matching default privileges from the same schema as the privilege being checked.
A possible fix would be to pass the schema context to isPrivilegeCoveredByDefaultPrivileges or filter the default privileges list to only include those from the same schema before calling the function.