-
Notifications
You must be signed in to change notification settings - Fork 29
feat: add support for column-level GRANT privileges #238
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 |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| package diff | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/pgschema/pgschema/ir" | ||
| ) | ||
|
|
||
| // generateCreateColumnPrivilegesSQL generates GRANT statements for new column privileges | ||
| func generateCreateColumnPrivilegesSQL(privileges []*ir.ColumnPrivilege, targetSchema string, collector *diffCollector) { | ||
| for _, cp := range privileges { | ||
| sql := generateGrantColumnPrivilegeSQL(cp) | ||
|
|
||
| // Path format: column_privileges.TABLE.{table_name}.{columns}.{grantee} | ||
| sortedCols := make([]string, len(cp.Columns)) | ||
| copy(sortedCols, cp.Columns) | ||
| sort.Strings(sortedCols) | ||
| colKey := strings.Join(sortedCols, ",") | ||
|
|
||
| context := &diffContext{ | ||
| Type: DiffTypeColumnPrivilege, | ||
| Operation: DiffOperationCreate, | ||
| Path: fmt.Sprintf("column_privileges.TABLE.%s.%s.%s", cp.TableName, colKey, cp.Grantee), | ||
| Source: cp, | ||
| CanRunInTransaction: true, | ||
| } | ||
|
|
||
| collector.collect(context, sql) | ||
| } | ||
| } | ||
|
|
||
| // generateDropColumnPrivilegesSQL generates REVOKE statements for removed column privileges | ||
| func generateDropColumnPrivilegesSQL(privileges []*ir.ColumnPrivilege, targetSchema string, collector *diffCollector) { | ||
| for _, cp := range privileges { | ||
| sql := generateRevokeColumnPrivilegeSQL(cp) | ||
|
|
||
| sortedCols := make([]string, len(cp.Columns)) | ||
| copy(sortedCols, cp.Columns) | ||
| sort.Strings(sortedCols) | ||
| colKey := strings.Join(sortedCols, ",") | ||
|
|
||
| context := &diffContext{ | ||
| Type: DiffTypeColumnPrivilege, | ||
| Operation: DiffOperationDrop, | ||
| Path: fmt.Sprintf("column_privileges.TABLE.%s.%s.%s", cp.TableName, colKey, cp.Grantee), | ||
| Source: cp, | ||
| CanRunInTransaction: true, | ||
| } | ||
|
|
||
| collector.collect(context, sql) | ||
| } | ||
| } | ||
|
|
||
| // generateModifyColumnPrivilegesSQL generates ALTER column privilege statements for modifications | ||
| func generateModifyColumnPrivilegesSQL(diffs []*columnPrivilegeDiff, targetSchema string, collector *diffCollector) { | ||
| for _, diff := range diffs { | ||
| statements := diff.generateAlterColumnPrivilegeStatements() | ||
|
|
||
| sortedCols := make([]string, len(diff.New.Columns)) | ||
| copy(sortedCols, diff.New.Columns) | ||
| sort.Strings(sortedCols) | ||
| colKey := strings.Join(sortedCols, ",") | ||
|
|
||
| for _, stmt := range statements { | ||
| context := &diffContext{ | ||
| Type: DiffTypeColumnPrivilege, | ||
| Operation: DiffOperationAlter, | ||
| Path: fmt.Sprintf("column_privileges.TABLE.%s.%s.%s", diff.New.TableName, colKey, diff.New.Grantee), | ||
| Source: diff, | ||
| CanRunInTransaction: true, | ||
| } | ||
|
|
||
| collector.collect(context, stmt) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // generateGrantColumnPrivilegeSQL generates a GRANT statement for column privileges | ||
| func generateGrantColumnPrivilegeSQL(cp *ir.ColumnPrivilege) string { | ||
| // Sort privileges for deterministic output | ||
| sortedPrivs := make([]string, len(cp.Privileges)) | ||
| copy(sortedPrivs, cp.Privileges) | ||
| sort.Strings(sortedPrivs) | ||
|
|
||
| privStr := strings.Join(sortedPrivs, ", ") | ||
|
|
||
| // Format columns with proper quoting | ||
| quotedCols := make([]string, len(cp.Columns)) | ||
| for i, col := range cp.Columns { | ||
| quotedCols[i] = ir.QuoteIdentifier(col) | ||
| } | ||
| sort.Strings(quotedCols) | ||
| colStr := strings.Join(quotedCols, ", ") | ||
|
|
||
| grantee := formatGrantee(cp.Grantee) | ||
| tableName := ir.QuoteIdentifier(cp.TableName) | ||
|
|
||
| sql := fmt.Sprintf("GRANT %s (%s) ON TABLE %s TO %s", privStr, colStr, tableName, grantee) | ||
|
|
||
| if cp.WithGrantOption { | ||
| sql += " WITH GRANT OPTION" | ||
| } | ||
|
|
||
| return sql + ";" | ||
| } | ||
|
|
||
| // generateRevokeColumnPrivilegeSQL generates a REVOKE statement for column privileges | ||
| func generateRevokeColumnPrivilegeSQL(cp *ir.ColumnPrivilege) string { | ||
| // Sort privileges for deterministic output | ||
| sortedPrivs := make([]string, len(cp.Privileges)) | ||
| copy(sortedPrivs, cp.Privileges) | ||
| sort.Strings(sortedPrivs) | ||
|
|
||
| privStr := strings.Join(sortedPrivs, ", ") | ||
|
|
||
| // Format columns with proper quoting | ||
| quotedCols := make([]string, len(cp.Columns)) | ||
| for i, col := range cp.Columns { | ||
| quotedCols[i] = ir.QuoteIdentifier(col) | ||
| } | ||
| sort.Strings(quotedCols) | ||
| colStr := strings.Join(quotedCols, ", ") | ||
|
Comment on lines
+119
to
+124
|
||
|
|
||
| grantee := formatGrantee(cp.Grantee) | ||
| tableName := ir.QuoteIdentifier(cp.TableName) | ||
|
|
||
| return fmt.Sprintf("REVOKE %s (%s) ON TABLE %s FROM %s;", privStr, colStr, tableName, grantee) | ||
| } | ||
|
|
||
| // generateAlterColumnPrivilegeStatements generates statements for column privilege modifications | ||
| func (d *columnPrivilegeDiff) generateAlterColumnPrivilegeStatements() []string { | ||
| var statements []string | ||
|
|
||
| // Find privileges to revoke (in old but not in new) | ||
| oldPrivSet := make(map[string]bool) | ||
| for _, p := range d.Old.Privileges { | ||
| oldPrivSet[p] = true | ||
| } | ||
| newPrivSet := make(map[string]bool) | ||
| for _, p := range d.New.Privileges { | ||
| newPrivSet[p] = true | ||
| } | ||
|
|
||
| var toRevoke []string | ||
| for p := range oldPrivSet { | ||
| if !newPrivSet[p] { | ||
| toRevoke = append(toRevoke, p) | ||
| } | ||
| } | ||
|
|
||
| var toGrant []string | ||
| for p := range newPrivSet { | ||
| if !oldPrivSet[p] { | ||
| toGrant = append(toGrant, p) | ||
| } | ||
| } | ||
|
|
||
| grantee := formatGrantee(d.New.Grantee) | ||
| tableName := ir.QuoteIdentifier(d.New.TableName) | ||
|
|
||
| // Format columns with proper quoting | ||
| quotedCols := make([]string, len(d.New.Columns)) | ||
| for i, col := range d.New.Columns { | ||
| quotedCols[i] = ir.QuoteIdentifier(col) | ||
| } | ||
| sort.Strings(quotedCols) | ||
| colStr := strings.Join(quotedCols, ", ") | ||
|
Comment on lines
+164
to
+169
|
||
|
|
||
| // Generate REVOKE for removed privileges | ||
| if len(toRevoke) > 0 { | ||
| sort.Strings(toRevoke) | ||
| statements = append(statements, fmt.Sprintf("REVOKE %s (%s) ON TABLE %s FROM %s;", | ||
| strings.Join(toRevoke, ", "), colStr, tableName, grantee)) | ||
| } | ||
|
|
||
| // Generate GRANT for added privileges | ||
| if len(toGrant) > 0 { | ||
| sort.Strings(toGrant) | ||
| sql := fmt.Sprintf("GRANT %s (%s) ON TABLE %s TO %s", | ||
| strings.Join(toGrant, ", "), colStr, tableName, grantee) | ||
| if d.New.WithGrantOption { | ||
| sql += " WITH GRANT OPTION" | ||
| } | ||
| statements = append(statements, sql+";") | ||
| } | ||
|
|
||
| // Handle WITH GRANT OPTION changes for unchanged privileges | ||
| if d.Old.WithGrantOption != d.New.WithGrantOption { | ||
| // Find unchanged privileges (in both old and new) | ||
| var unchanged []string | ||
| for p := range oldPrivSet { | ||
| if newPrivSet[p] { | ||
| unchanged = append(unchanged, p) | ||
| } | ||
| } | ||
|
|
||
| if len(unchanged) > 0 { | ||
| sort.Strings(unchanged) | ||
| unchangedStr := strings.Join(unchanged, ", ") | ||
|
|
||
| if d.Old.WithGrantOption && !d.New.WithGrantOption { | ||
| // Revoke grant option only (keep the privilege) | ||
| statements = append(statements, fmt.Sprintf("REVOKE GRANT OPTION FOR %s (%s) ON TABLE %s FROM %s;", | ||
| unchangedStr, colStr, tableName, grantee)) | ||
| } else if !d.Old.WithGrantOption && d.New.WithGrantOption { | ||
| // Add grant option (re-grant with grant option) | ||
| statements = append(statements, fmt.Sprintf("GRANT %s (%s) ON TABLE %s TO %s WITH GRANT OPTION;", | ||
| unchangedStr, colStr, tableName, grantee)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return statements | ||
| } | ||
|
|
||
| // columnPrivilegesEqual checks if two column privileges are structurally equal | ||
| func columnPrivilegesEqual(old, new *ir.ColumnPrivilege) bool { | ||
| if old.TableName != new.TableName { | ||
| return false | ||
| } | ||
| if old.Grantee != new.Grantee { | ||
| return false | ||
| } | ||
| if old.WithGrantOption != new.WithGrantOption { | ||
| return false | ||
| } | ||
|
|
||
| // Compare columns (order-independent) | ||
| if len(old.Columns) != len(new.Columns) { | ||
| return false | ||
| } | ||
| oldColSet := make(map[string]bool) | ||
| for _, c := range old.Columns { | ||
| oldColSet[c] = true | ||
| } | ||
| for _, c := range new.Columns { | ||
| if !oldColSet[c] { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // Compare privileges (order-independent) | ||
| if len(old.Privileges) != len(new.Privileges) { | ||
| return false | ||
| } | ||
| oldPrivSet := make(map[string]bool) | ||
| for _, p := range old.Privileges { | ||
| oldPrivSet[p] = true | ||
| } | ||
| for _, p := range new.Privileges { | ||
| if !oldPrivSet[p] { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| // GetObjectName returns a unique identifier for the column privilege diff | ||
| func (d *columnPrivilegeDiff) GetObjectName() string { | ||
| return d.New.GetObjectKey() | ||
| } | ||
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 columns are being sorted AFTER quoting, which could lead to incorrect sort order. Since cp.Columns is already sorted (as documented in the ColumnPrivilege struct), the quotedCols should maintain that order. Sorting quoted identifiers can produce a different order than sorting unquoted identifiers, especially with case-sensitive or special characters.
The sort.Strings(quotedCols) on line 94 should be removed. The columns should be quoted in the order they appear in cp.Columns, which is already sorted.