diff --git a/internal/diff/diff.go b/internal/diff/diff.go index b24e0cba..10782a22 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -37,6 +37,8 @@ const ( DiffTypeDomain DiffTypeComment DiffTypeDefaultPrivilege + DiffTypePrivilege + DiffTypeRevokedDefaultPrivilege ) // String returns the string representation of DiffType @@ -88,6 +90,10 @@ func (d DiffType) String() string { return "comment" case DiffTypeDefaultPrivilege: return "default_privilege" + case DiffTypePrivilege: + return "privilege" + case DiffTypeRevokedDefaultPrivilege: + return "revoked_default_privilege" default: return "unknown" } @@ -152,6 +158,10 @@ func (d *DiffType) UnmarshalJSON(data []byte) error { *d = DiffTypeComment case "default_privilege": *d = DiffTypeDefaultPrivilege + case "privilege": + *d = DiffTypePrivilege + case "revoked_default_privilege": + *d = DiffTypeRevokedDefaultPrivilege default: return fmt.Errorf("unknown diff type: %s", s) } @@ -259,6 +269,12 @@ type ddlDiff struct { addedDefaultPrivileges []*ir.DefaultPrivilege droppedDefaultPrivileges []*ir.DefaultPrivilege modifiedDefaultPrivileges []*defaultPrivilegeDiff + // Explicit object privileges + addedPrivileges []*ir.Privilege + droppedPrivileges []*ir.Privilege + modifiedPrivileges []*privilegeDiff + addedRevokedDefaultPrivs []*ir.RevokedDefaultPrivilege + droppedRevokedDefaultPrivs []*ir.RevokedDefaultPrivilege } // schemaDiff represents changes to a schema @@ -297,6 +313,12 @@ type defaultPrivilegeDiff struct { New *ir.DefaultPrivilege } +// privilegeDiff represents changes to explicit object privileges +type privilegeDiff struct { + Old *ir.Privilege + New *ir.Privilege +} + // triggerDiff represents changes to a trigger type triggerDiff struct { Old *ir.Trigger @@ -398,6 +420,11 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff { addedDefaultPrivileges: []*ir.DefaultPrivilege{}, droppedDefaultPrivileges: []*ir.DefaultPrivilege{}, modifiedDefaultPrivileges: []*defaultPrivilegeDiff{}, + addedPrivileges: []*ir.Privilege{}, + droppedPrivileges: []*ir.Privilege{}, + modifiedPrivileges: []*privilegeDiff{}, + addedRevokedDefaultPrivs: []*ir.RevokedDefaultPrivilege{}, + droppedRevokedDefaultPrivs: []*ir.RevokedDefaultPrivilege{}, } // Compare schemas first in deterministic order @@ -970,6 +997,144 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff { return diff.modifiedDefaultPrivileges[i].New.Grantee < diff.modifiedDefaultPrivileges[j].New.Grantee }) + // Compare explicit object privileges across all schemas + // Use GetFullKey() to avoid overwrites when same (object, grantee) has different grant options + oldPrivs := make(map[string]*ir.Privilege) + newPrivs := make(map[string]*ir.Privilege) + + for _, dbSchema := range oldIR.Schemas { + for _, p := range dbSchema.Privileges { + key := p.GetFullKey() + oldPrivs[key] = p + } + } + + for _, dbSchema := range newIR.Schemas { + for _, p := range dbSchema.Privileges { + key := p.GetFullKey() + newPrivs[key] = p + } + } + + // Build index by GetObjectKey() to find matching privileges for modification detection + oldPrivsByObjectKey := make(map[string][]*ir.Privilege) + newPrivsByObjectKey := make(map[string][]*ir.Privilege) + for _, p := range oldPrivs { + key := p.GetObjectKey() + oldPrivsByObjectKey[key] = append(oldPrivsByObjectKey[key], p) + } + for _, p := range newPrivs { + key := p.GetObjectKey() + newPrivsByObjectKey[key] = append(newPrivsByObjectKey[key], p) + } + + // Track which privileges have been matched for modification + matchedOld := make(map[string]bool) + matchedNew := make(map[string]bool) + + // Find modified privileges - match by GetObjectKey() to detect grant option changes + for objectKey, newList := range newPrivsByObjectKey { + oldList := oldPrivsByObjectKey[objectKey] + if len(oldList) == 0 { + continue + } + + // Simple case: one privilege each, check for modification + if len(oldList) == 1 && len(newList) == 1 { + oldP, newP := oldList[0], newList[0] + if !privilegesEqual(oldP, newP) { + diff.modifiedPrivileges = append(diff.modifiedPrivileges, &privilegeDiff{ + Old: oldP, + New: newP, + }) + } + matchedOld[oldP.GetFullKey()] = true + matchedNew[newP.GetFullKey()] = true + continue + } + + // Complex case: multiple privileges with same object key but different grant options + // Match by full key first, then handle remaining as add/drop + for _, newP := range newList { + fullKey := newP.GetFullKey() + if oldP, exists := oldPrivs[fullKey]; exists { + if !privilegesEqual(oldP, newP) { + diff.modifiedPrivileges = append(diff.modifiedPrivileges, &privilegeDiff{ + Old: oldP, + New: newP, + }) + } + matchedOld[fullKey] = true + matchedNew[fullKey] = true + } + } + } + + // Find added privileges (in new but not matched) + for fullKey, p := range newPrivs { + if !matchedNew[fullKey] { + diff.addedPrivileges = append(diff.addedPrivileges, p) + } + } + + // Find dropped privileges (in old but not matched) + for fullKey, p := range oldPrivs { + if !matchedOld[fullKey] { + diff.droppedPrivileges = append(diff.droppedPrivileges, p) + } + } + + // Sort privileges for deterministic output + sort.Slice(diff.addedPrivileges, func(i, j int) bool { + return diff.addedPrivileges[i].GetObjectKey() < diff.addedPrivileges[j].GetObjectKey() + }) + sort.Slice(diff.droppedPrivileges, func(i, j int) bool { + return diff.droppedPrivileges[i].GetObjectKey() < diff.droppedPrivileges[j].GetObjectKey() + }) + sort.Slice(diff.modifiedPrivileges, func(i, j int) bool { + return diff.modifiedPrivileges[i].New.GetObjectKey() < diff.modifiedPrivileges[j].New.GetObjectKey() + }) + + // Compare revoked default privileges across all schemas + oldRevokedPrivs := make(map[string]*ir.RevokedDefaultPrivilege) + newRevokedPrivs := make(map[string]*ir.RevokedDefaultPrivilege) + + for _, dbSchema := range oldIR.Schemas { + for _, r := range dbSchema.RevokedDefaultPrivileges { + key := r.GetObjectKey() + oldRevokedPrivs[key] = r + } + } + + for _, dbSchema := range newIR.Schemas { + for _, r := range dbSchema.RevokedDefaultPrivileges { + key := r.GetObjectKey() + newRevokedPrivs[key] = r + } + } + + // Find added revoked default privileges (new revokes) + for key, r := range newRevokedPrivs { + if _, exists := oldRevokedPrivs[key]; !exists { + diff.addedRevokedDefaultPrivs = append(diff.addedRevokedDefaultPrivs, r) + } + } + + // Find dropped revoked default privileges (restored defaults) + for key, r := range oldRevokedPrivs { + if _, exists := newRevokedPrivs[key]; !exists { + diff.droppedRevokedDefaultPrivs = append(diff.droppedRevokedDefaultPrivs, r) + } + } + + // Sort revoked default privileges for deterministic output + sort.Slice(diff.addedRevokedDefaultPrivs, func(i, j int) bool { + return diff.addedRevokedDefaultPrivs[i].GetObjectKey() < diff.addedRevokedDefaultPrivs[j].GetObjectKey() + }) + sort.Slice(diff.droppedRevokedDefaultPrivs, func(i, j int) bool { + return diff.droppedRevokedDefaultPrivs[i].GetObjectKey() < diff.droppedRevokedDefaultPrivs[j].GetObjectKey() + }) + // Sort tables and views topologically for consistent ordering // Pre-sort by name to ensure deterministic insertion order for cycle breaking sort.Slice(diff.addedTables, func(i, j int) bool { @@ -1176,6 +1341,12 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto // Create default privileges generateCreateDefaultPrivilegesSQL(d.addedDefaultPrivileges, targetSchema, collector) + + // Create explicit object privileges + generateCreatePrivilegesSQL(d.addedPrivileges, targetSchema, collector) + + // Revoke default PUBLIC privileges (new revokes) + generateRevokeDefaultPrivilegesSQL(d.addedRevokedDefaultPrivs, targetSchema, collector) } // generateModifySQL generates ALTER statements @@ -1204,6 +1375,9 @@ func (d *ddlDiff) generateModifySQL(targetSchema string, collector *diffCollecto // Modify default privileges generateModifyDefaultPrivilegesSQL(d.modifiedDefaultPrivileges, targetSchema, collector) + + // Modify explicit object privileges + generateModifyPrivilegesSQL(d.modifiedPrivileges, targetSchema, collector) } // generateDropSQL generates DROP statements in reverse dependency order @@ -1232,6 +1406,12 @@ func (d *ddlDiff) generateDropSQL(targetSchema string, collector *diffCollector, // Drop types generateDropTypesSQL(d.droppedTypes, targetSchema, collector) + // Restore default PUBLIC privileges (dropped revokes = restore defaults) + generateRestoreDefaultPrivilegesSQL(d.droppedRevokedDefaultPrivs, targetSchema, collector) + + // Drop explicit object privileges + generateDropPrivilegesSQL(d.droppedPrivileges, targetSchema, collector) + // Drop default privileges generateDropDefaultPrivilegesSQL(d.droppedDefaultPrivileges, targetSchema, collector) diff --git a/internal/diff/privilege.go b/internal/diff/privilege.go new file mode 100644 index 00000000..c523f8b4 --- /dev/null +++ b/internal/diff/privilege.go @@ -0,0 +1,296 @@ +package diff + +import ( + "fmt" + "sort" + "strings" + + "github.com/pgschema/pgschema/ir" +) + +// generateCreatePrivilegesSQL generates GRANT statements for new privileges +func generateCreatePrivilegesSQL(privileges []*ir.Privilege, targetSchema string, collector *diffCollector) { + for _, p := range privileges { + sql := generateGrantPrivilegeSQL(p) + + context := &diffContext{ + Type: DiffTypePrivilege, + Operation: DiffOperationCreate, + Path: fmt.Sprintf("privileges.%s.%s.%s", p.ObjectType, p.ObjectName, p.Grantee), + Source: p, + CanRunInTransaction: true, + } + + collector.collect(context, sql) + } +} + +// generateDropPrivilegesSQL generates REVOKE statements for removed privileges +func generateDropPrivilegesSQL(privileges []*ir.Privilege, targetSchema string, collector *diffCollector) { + for _, p := range privileges { + sql := generateRevokePrivilegeSQL(p) + + context := &diffContext{ + Type: DiffTypePrivilege, + Operation: DiffOperationDrop, + Path: fmt.Sprintf("privileges.%s.%s.%s", p.ObjectType, p.ObjectName, p.Grantee), + Source: p, + CanRunInTransaction: true, + } + + collector.collect(context, sql) + } +} + +// generateModifyPrivilegesSQL generates ALTER privilege statements for modifications +func generateModifyPrivilegesSQL(diffs []*privilegeDiff, targetSchema string, collector *diffCollector) { + for _, diff := range diffs { + statements := diff.generateAlterPrivilegeStatements() + for _, stmt := range statements { + context := &diffContext{ + Type: DiffTypePrivilege, + Operation: DiffOperationAlter, + Path: fmt.Sprintf("privileges.%s.%s.%s", diff.New.ObjectType, diff.New.ObjectName, diff.New.Grantee), + Source: diff, + CanRunInTransaction: true, + } + + collector.collect(context, stmt) + } + } +} + +// generateGrantPrivilegeSQL generates a GRANT statement +func generateGrantPrivilegeSQL(p *ir.Privilege) string { + // Sort privileges for deterministic output + sortedPrivs := make([]string, len(p.Privileges)) + copy(sortedPrivs, p.Privileges) + sort.Strings(sortedPrivs) + + privStr := strings.Join(sortedPrivs, ", ") + grantee := formatGrantee(p.Grantee) + objectRef := formatObjectReference(p.ObjectType, p.ObjectName) + + sql := fmt.Sprintf("GRANT %s ON %s TO %s", privStr, objectRef, grantee) + + if p.WithGrantOption { + sql += " WITH GRANT OPTION" + } + + return sql + ";" +} + +// generateRevokePrivilegeSQL generates a REVOKE statement +func generateRevokePrivilegeSQL(p *ir.Privilege) string { + // Sort privileges for deterministic output + sortedPrivs := make([]string, len(p.Privileges)) + copy(sortedPrivs, p.Privileges) + sort.Strings(sortedPrivs) + + privStr := strings.Join(sortedPrivs, ", ") + grantee := formatGrantee(p.Grantee) + objectRef := formatObjectReference(p.ObjectType, p.ObjectName) + + return fmt.Sprintf("REVOKE %s ON %s FROM %s;", privStr, objectRef, grantee) +} + +// generateAlterPrivilegeStatements generates statements for privilege modifications +func (d *privilegeDiff) generateAlterPrivilegeStatements() []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) + objectRef := formatObjectReference(d.New.ObjectType, d.New.ObjectName) + + // Generate REVOKE for removed privileges + if len(toRevoke) > 0 { + sort.Strings(toRevoke) + statements = append(statements, fmt.Sprintf("REVOKE %s ON %s FROM %s;", + strings.Join(toRevoke, ", "), objectRef, grantee)) + } + + // Generate GRANT for added privileges + if len(toGrant) > 0 { + sort.Strings(toGrant) + sql := fmt.Sprintf("GRANT %s ON %s TO %s", strings.Join(toGrant, ", "), objectRef, 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 ON %s FROM %s;", + unchangedStr, objectRef, grantee)) + } else if !d.Old.WithGrantOption && d.New.WithGrantOption { + // Add grant option (re-grant with grant option) + statements = append(statements, fmt.Sprintf("GRANT %s ON %s TO %s WITH GRANT OPTION;", + unchangedStr, objectRef, grantee)) + } + } + } + + return statements +} + +// generateRevokeDefaultPrivilegesSQL generates REVOKE statements for revoking default PUBLIC grants +func generateRevokeDefaultPrivilegesSQL(revoked []*ir.RevokedDefaultPrivilege, targetSchema string, collector *diffCollector) { + for _, r := range revoked { + sql := generateRevokeDefaultPublicSQL(r) + + context := &diffContext{ + Type: DiffTypeRevokedDefaultPrivilege, + Operation: DiffOperationCreate, // Creating a revoke + Path: fmt.Sprintf("revoked_default.%s.%s", r.ObjectType, r.ObjectName), + Source: r, + CanRunInTransaction: true, + } + + collector.collect(context, sql) + } +} + +// generateRestoreDefaultPrivilegesSQL generates GRANT statements to restore default PUBLIC grants +func generateRestoreDefaultPrivilegesSQL(revoked []*ir.RevokedDefaultPrivilege, targetSchema string, collector *diffCollector) { + for _, r := range revoked { + sql := generateGrantDefaultPublicSQL(r) + + context := &diffContext{ + Type: DiffTypeRevokedDefaultPrivilege, + Operation: DiffOperationDrop, // Dropping a revoke = restoring default + Path: fmt.Sprintf("revoked_default.%s.%s", r.ObjectType, r.ObjectName), + Source: r, + CanRunInTransaction: true, + } + + collector.collect(context, sql) + } +} + +// generateRevokeDefaultPublicSQL generates REVOKE ... FROM PUBLIC statement +func generateRevokeDefaultPublicSQL(r *ir.RevokedDefaultPrivilege) string { + sortedPrivs := make([]string, len(r.Privileges)) + copy(sortedPrivs, r.Privileges) + sort.Strings(sortedPrivs) + + privStr := strings.Join(sortedPrivs, ", ") + objectRef := formatObjectReference(r.ObjectType, r.ObjectName) + + return fmt.Sprintf("REVOKE %s ON %s FROM PUBLIC;", privStr, objectRef) +} + +// generateGrantDefaultPublicSQL generates GRANT ... TO PUBLIC statement (restore default) +func generateGrantDefaultPublicSQL(r *ir.RevokedDefaultPrivilege) string { + sortedPrivs := make([]string, len(r.Privileges)) + copy(sortedPrivs, r.Privileges) + sort.Strings(sortedPrivs) + + privStr := strings.Join(sortedPrivs, ", ") + objectRef := formatObjectReference(r.ObjectType, r.ObjectName) + + return fmt.Sprintf("GRANT %s ON %s TO PUBLIC;", privStr, objectRef) +} + +// formatGrantee formats the grantee for use in GRANT/REVOKE statements +func formatGrantee(grantee string) string { + if grantee == "" || grantee == "PUBLIC" { + return "PUBLIC" + } + return ir.QuoteIdentifier(grantee) +} + +// formatObjectReference formats the object reference for GRANT/REVOKE statements +func formatObjectReference(objType ir.PrivilegeObjectType, objName string) string { + switch objType { + case ir.PrivilegeObjectTypeTable: + return "TABLE " + ir.QuoteIdentifier(objName) + case ir.PrivilegeObjectTypeView: + return "TABLE " + ir.QuoteIdentifier(objName) // Views use TABLE keyword in GRANT + case ir.PrivilegeObjectTypeSequence: + return "SEQUENCE " + ir.QuoteIdentifier(objName) + case ir.PrivilegeObjectTypeFunction: + return "FUNCTION " + objName // Function signature already includes parentheses + case ir.PrivilegeObjectTypeProcedure: + return "PROCEDURE " + objName // Procedure signature already includes parentheses + case ir.PrivilegeObjectTypeType: + return "TYPE " + ir.QuoteIdentifier(objName) + default: + return ir.QuoteIdentifier(objName) + } +} + +// privilegesEqual checks if two privileges are structurally equal +func privilegesEqual(old, new *ir.Privilege) bool { + if old.ObjectType != new.ObjectType { + return false + } + if old.ObjectName != new.ObjectName { + return false + } + if old.Grantee != new.Grantee { + return false + } + if old.WithGrantOption != new.WithGrantOption { + 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 privilege diff +func (d *privilegeDiff) GetObjectName() string { + return d.New.GetObjectKey() +} diff --git a/internal/dump/formatter.go b/internal/dump/formatter.go index 91d95175..791c392d 100644 --- a/internal/dump/formatter.go +++ b/internal/dump/formatter.go @@ -101,7 +101,7 @@ func (f *DumpFormatter) FormatMultiFile(diffs []diff.Diff, outputPath string) er } // Create files in dependency order - orderedDirs := []string{"types", "domains", "sequences", "functions", "procedures", "tables", "views", "materialized_views", "default_privileges"} + orderedDirs := []string{"types", "domains", "sequences", "functions", "procedures", "tables", "views", "materialized_views", "default_privileges", "privileges"} for _, dir := range orderedDirs { if objects, exists := filesByType[dir]; exists { @@ -242,6 +242,8 @@ func (f *DumpFormatter) getObjectDirectory(objectType string) string { return "tables" // fallback, will be overridden case "default_privilege": return "default_privileges" + case "privilege", "revoked_default_privilege": + return "privileges" default: return "misc" } @@ -326,6 +328,30 @@ func (f *DumpFormatter) getGroupingName(step diff.Diff) string { if parts := strings.Split(step.Path, "."); len(parts) >= 2 { return parts[1] // Return object type } + case diff.DiffTypePrivilege: + // For explicit privileges, group by object type + if step.Source != nil { + switch obj := step.Source.(type) { + case *ir.Privilege: + return string(obj.ObjectType) // Group by TABLE, FUNCTION, etc. + } + } + // Fallback: extract from path (privileges.TABLE.name.grantee) + if parts := strings.Split(step.Path, "."); len(parts) >= 2 { + return parts[1] // Return object type + } + case diff.DiffTypeRevokedDefaultPrivilege: + // For revoked default privileges, group by object type + if step.Source != nil { + switch obj := step.Source.(type) { + case *ir.RevokedDefaultPrivilege: + return string(obj.ObjectType) // Group by FUNCTION, TYPE, etc. + } + } + // Fallback: extract from path (revoked_default.FUNCTION.name) + if parts := strings.Split(step.Path, "."); len(parts) >= 2 { + return parts[1] // Return object type + } } // For standalone objects or if table name extraction fails, use object name diff --git a/internal/plan/plan.go b/internal/plan/plan.go index e3155b21..6f6c8bcb 100644 --- a/internal/plan/plan.go +++ b/internal/plan/plan.go @@ -99,8 +99,10 @@ const ( TypeTrigger Type = "triggers" TypePolicy Type = "policies" TypeColumn Type = "columns" - TypeRLS Type = "rls" - TypeDefaultPrivilege Type = "default privileges" + TypeRLS Type = "rls" + TypeDefaultPrivilege Type = "default privileges" + TypePrivilege Type = "privileges" + TypeRevokedDefaultPrivilege Type = "revoked default privileges" ) // SQLFormat represents the different output formats for SQL generation @@ -130,6 +132,8 @@ func getObjectOrder() []Type { TypePolicy, TypeColumn, TypeRLS, + TypePrivilege, + TypeRevokedDefaultPrivilege, } } diff --git a/ir/inspector.go b/ir/inspector.go index 5ab8986d..12b01ff2 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -71,6 +71,7 @@ func (i *Inspector) BuildIR(ctx context.Context, targetSchema string) (*IR, erro i.buildAggregates, i.buildTypes, i.buildDefaultPrivileges, + i.buildPrivileges, }, } @@ -1863,6 +1864,181 @@ func (i *Inspector) validateSchemaExists(ctx context.Context, schemaName string) return nil } +// buildPrivileges retrieves explicit privilege grants for objects in the schema +func (i *Inspector) buildPrivileges(ctx context.Context, schema *IR, targetSchema string) error { + rows, err := i.queries.GetPrivilegesForSchema(ctx, sql.NullString{String: targetSchema, Valid: true}) + if err != nil { + return fmt.Errorf("failed to query privileges: %w", err) + } + + // Group privileges by (object_type, object_name, grantee, is_grantable) + type privKey struct { + ObjectType string + ObjectName string + Grantee string + WithGrantOption bool + } + + grouped := make(map[privKey][]string) + + for _, row := range rows { + objectName := row.ObjectName.String + objectType := row.ObjectType.String + privilegeType := row.PrivilegeType.String + owner := row.Owner.String + isGrantable := row.IsGrantable.Valid && row.IsGrantable.Bool + + // Determine grantee name from OID + grantee := "PUBLIC" + if row.GranteeOid != nil { + // Handle different possible types for grantee OID + var granteeOID int64 + switch v := row.GranteeOid.(type) { + case int64: + granteeOID = v + case int32: + granteeOID = int64(v) + case int: + granteeOID = int64(v) + } + if granteeOID != 0 { + // Look up role name + var roleName string + err := i.db.QueryRowContext(ctx, "SELECT rolname FROM pg_roles WHERE oid = $1", granteeOID).Scan(&roleName) + if err != nil { + if err == sql.ErrNoRows { + continue // Role no longer exists, skip this privilege + } + return fmt.Errorf("failed to lookup role name for OID %d: %w", granteeOID, err) + } + grantee = roleName + } + } + + // Skip owner grants (owners implicitly have all privileges) + if grantee == owner { + continue + } + + // Check for default PUBLIC grants that should be excluded + if grantee == "PUBLIC" { + if (objectType == "FUNCTION" || objectType == "PROCEDURE") && privilegeType == "EXECUTE" { + // Default PUBLIC EXECUTE on functions/procedures - skip + continue + } + if objectType == "TYPE" && privilegeType == "USAGE" { + // Default PUBLIC USAGE on types - skip + continue + } + } + + key := privKey{ + ObjectType: objectType, + ObjectName: objectName, + Grantee: grantee, + WithGrantOption: isGrantable, + } + + grouped[key] = append(grouped[key], privilegeType) + } + + // Convert to Privilege structs + var privileges []*Privilege + for key, privs := range grouped { + // Sort privileges for deterministic output + sort.Strings(privs) + + var objType PrivilegeObjectType + switch key.ObjectType { + case "TABLE": + objType = PrivilegeObjectTypeTable + case "VIEW": + objType = PrivilegeObjectTypeView + case "SEQUENCE": + objType = PrivilegeObjectTypeSequence + case "FUNCTION": + objType = PrivilegeObjectTypeFunction + case "PROCEDURE": + objType = PrivilegeObjectTypeProcedure + case "TYPE": + objType = PrivilegeObjectTypeType + default: + continue + } + + p := &Privilege{ + ObjectType: objType, + ObjectName: key.ObjectName, + Grantee: key.Grantee, + Privileges: privs, + WithGrantOption: key.WithGrantOption, + } + privileges = append(privileges, p) + } + + // Sort for deterministic output + sort.Slice(privileges, func(i, j int) bool { + if privileges[i].ObjectType != privileges[j].ObjectType { + return privileges[i].ObjectType < privileges[j].ObjectType + } + if privileges[i].ObjectName != privileges[j].ObjectName { + return privileges[i].ObjectName < privileges[j].ObjectName + } + return privileges[i].Grantee < privileges[j].Grantee + }) + + // Now check for revoked default privileges (functions/procedures/types where PUBLIC has no access) + revokedDefaults, err := i.buildRevokedDefaultPrivileges(ctx, targetSchema) + if err != nil { + return fmt.Errorf("failed to build revoked default privileges: %w", err) + } + + // Assign to schema + s, ok := schema.GetSchema(targetSchema) + if ok { + s.Privileges = privileges + s.RevokedDefaultPrivileges = revokedDefaults + } + + return nil +} + +// buildRevokedDefaultPrivileges finds objects where default PUBLIC grants have been explicitly revoked +func (i *Inspector) buildRevokedDefaultPrivileges(ctx context.Context, targetSchema string) ([]*RevokedDefaultPrivilege, error) { + rows, err := i.queries.GetRevokedDefaultPrivilegesForSchema(ctx, sql.NullString{String: targetSchema, Valid: true}) + if err != nil { + return nil, fmt.Errorf("failed to query revoked default privileges: %w", err) + } + + var revoked []*RevokedDefaultPrivilege + for _, row := range rows { + objectName := row.ObjectName.String + objectType := row.ObjectType.String + + var objType PrivilegeObjectType + var privs []string + switch objectType { + case "FUNCTION", "PROCEDURE": + objType = PrivilegeObjectType(objectType) + privs = []string{"EXECUTE"} + case "TYPE": + objType = PrivilegeObjectTypeType + privs = []string{"USAGE"} + default: + continue + } + + r := &RevokedDefaultPrivilege{ + ObjectType: objType, + ObjectName: objectName, + Privileges: privs, + } + revoked = append(revoked, r) + } + + return revoked, nil +} + // buildDefaultPrivileges retrieves default privileges for the schema func (i *Inspector) buildDefaultPrivileges(ctx context.Context, schema *IR, targetSchema string) error { privileges, err := i.queries.GetDefaultPrivilegesForSchema(ctx, sql.NullString{String: targetSchema, Valid: true}) diff --git a/ir/ir.go b/ir/ir.go index 43f2425f..2e1555a5 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -22,15 +22,17 @@ type Schema struct { Name string `json:"name"` Owner string `json:"owner"` // Schema owner // Note: Indexes, Triggers, and RLS Policies are stored at table level (Table.Indexes, Table.Triggers, Table.Policies) - Tables map[string]*Table `json:"tables"` // table_name -> Table - Views map[string]*View `json:"views"` // view_name -> View - Functions map[string]*Function `json:"functions"` // function_name -> Function - Procedures map[string]*Procedure `json:"procedures"` // procedure_name -> Procedure - Aggregates map[string]*Aggregate `json:"aggregates"` // aggregate_name -> Aggregate - Sequences map[string]*Sequence `json:"sequences"` // sequence_name -> Sequence - Types map[string]*Type `json:"types"` // type_name -> Type - DefaultPrivileges []*DefaultPrivilege `json:"default_privileges,omitempty"` // Default privileges for future objects - mu sync.RWMutex // Protects concurrent access to all maps + Tables map[string]*Table `json:"tables"` // table_name -> Table + Views map[string]*View `json:"views"` // view_name -> View + Functions map[string]*Function `json:"functions"` // function_name -> Function + Procedures map[string]*Procedure `json:"procedures"` // procedure_name -> Procedure + Aggregates map[string]*Aggregate `json:"aggregates"` // aggregate_name -> Aggregate + Sequences map[string]*Sequence `json:"sequences"` // sequence_name -> Sequence + Types map[string]*Type `json:"types"` // type_name -> Type + DefaultPrivileges []*DefaultPrivilege `json:"default_privileges,omitempty"` // Default privileges for future objects + Privileges []*Privilege `json:"privileges,omitempty"` // Explicit privilege grants on objects + RevokedDefaultPrivileges []*RevokedDefaultPrivilege `json:"revoked_default_privileges,omitempty"` // Explicit revokes of default PUBLIC privileges + mu sync.RWMutex // Protects concurrent access to all maps } // LikeClause represents a LIKE clause in CREATE TABLE statement @@ -425,6 +427,68 @@ func (d *DefaultPrivilege) GetObjectName() string { return string(d.ObjectType) + ":" + d.Grantee } +// PrivilegeObjectType represents the object type for explicit privilege grants +type PrivilegeObjectType string + +const ( + PrivilegeObjectTypeTable PrivilegeObjectType = "TABLE" + PrivilegeObjectTypeView PrivilegeObjectType = "VIEW" + PrivilegeObjectTypeSequence PrivilegeObjectType = "SEQUENCE" + PrivilegeObjectTypeFunction PrivilegeObjectType = "FUNCTION" + PrivilegeObjectTypeProcedure PrivilegeObjectType = "PROCEDURE" + PrivilegeObjectTypeType PrivilegeObjectType = "TYPE" +) + +// Privilege represents an explicit privilege grant on a schema object +type Privilege struct { + ObjectType PrivilegeObjectType `json:"object_type"` // TABLE, VIEW, SEQUENCE, FUNCTION, PROCEDURE, TYPE + ObjectName string `json:"object_name"` // table name or function signature + Grantee string `json:"grantee"` // role name or "PUBLIC" + Privileges []string `json:"privileges"` // [SELECT, INSERT, UPDATE, ...] or [EXECUTE] or [USAGE] + WithGrantOption bool `json:"with_grant_option"` // Can grantee grant to others? +} + +// GetObjectKey returns a unique identifier for the privilege (object + grantee) +// Note: This intentionally excludes WithGrantOption so that privilege modifications +// (e.g., adding or removing GRANT OPTION) are detected as modifications, not as +// separate add/drop operations. +func (p *Privilege) GetObjectKey() string { + return string(p.ObjectType) + ":" + p.ObjectName + ":" + p.Grantee +} + +// GetFullKey returns a unique identifier including WithGrantOption. +// Use this when you need to distinguish between the same privilege with different grant options. +func (p *Privilege) GetFullKey() string { + grantOption := "0" + if p.WithGrantOption { + grantOption = "1" + } + return string(p.ObjectType) + ":" + p.ObjectName + ":" + p.Grantee + ":" + grantOption +} + +// GetObjectName returns the object name for the privilege +func (p *Privilege) GetObjectName() string { + return p.ObjectName +} + +// RevokedDefaultPrivilege represents an explicit revoke of a default PUBLIC privilege +// This is used to track when default PUBLIC grants (e.g., EXECUTE on functions) are revoked +type RevokedDefaultPrivilege struct { + ObjectType PrivilegeObjectType `json:"object_type"` // FUNCTION, PROCEDURE, TYPE + ObjectName string `json:"object_name"` // function signature or type name + Privileges []string `json:"privileges"` // [EXECUTE] or [USAGE] +} + +// GetObjectKey returns a unique identifier for the revoked default privilege +func (r *RevokedDefaultPrivilege) GetObjectKey() string { + return string(r.ObjectType) + ":" + r.ObjectName +} + +// GetObjectName returns the object name for the revoked default privilege +func (r *RevokedDefaultPrivilege) GetObjectName() string { + return r.ObjectName +} + // NewIR creates a new empty catalog IR func NewIR() *IR { return &IR{ diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 6fc61080..33f6b587 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -1227,4 +1227,135 @@ SELECT a.is_grantable FROM acl_expanded a LEFT JOIN pg_roles r ON a.grantee_oid = r.oid -ORDER BY object_type, grantee, privilege_type; \ No newline at end of file +ORDER BY object_type, grantee, privilege_type; + +-- GetPrivilegesForSchema retrieves explicit privilege grants for objects in a specific schema +-- name: GetPrivilegesForSchema :many +WITH acl_data AS ( + -- Tables and Views + SELECT + n.nspname AS schema_name, + c.relname AS object_name, + CASE c.relkind + WHEN 'r' THEN 'TABLE' + WHEN 'v' THEN 'VIEW' + WHEN 'm' THEN 'VIEW' + WHEN 'S' THEN 'SEQUENCE' + END AS object_type, + c.relacl AS acl, + pg_get_userbyid(c.relowner) AS owner + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE n.nspname = $1 + AND c.relkind IN ('r', 'v', 'm', 'S') + AND c.relacl IS NOT NULL + + UNION ALL + + -- Functions + SELECT + n.nspname AS schema_name, + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'FUNCTION' AS object_type, + p.proacl AS acl, + pg_get_userbyid(p.proowner) AS owner + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'f' + AND p.proacl IS NOT NULL + + UNION ALL + + -- Procedures + SELECT + n.nspname AS schema_name, + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'PROCEDURE' AS object_type, + p.proacl AS acl, + pg_get_userbyid(p.proowner) AS owner + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'p' + AND p.proacl IS NOT NULL + + UNION ALL + + -- Types (ENUM, COMPOSITE, DOMAIN) + SELECT + n.nspname AS schema_name, + t.typname AS object_name, + 'TYPE' AS object_type, + t.typacl AS acl, + pg_get_userbyid(t.typowner) AS owner + FROM pg_type t + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE n.nspname = $1 + AND t.typtype IN ('e', 'c', 'd') + AND t.typacl IS NOT NULL +) +SELECT + schema_name, + object_name, + object_type, + (aclexplode(acl)).grantee AS grantee_oid, + (aclexplode(acl)).privilege_type AS privilege_type, + (aclexplode(acl)).is_grantable AS is_grantable, + owner +FROM acl_data +ORDER BY object_type, object_name, grantee_oid, privilege_type; + +-- GetRevokedDefaultPrivilegesForSchema finds objects where default PUBLIC grants have been explicitly revoked +-- name: GetRevokedDefaultPrivilegesForSchema :many +WITH objects_with_acl AS ( + -- Functions (ACL may be NULL for default permissions; filtering happens in public_grants CTE) + SELECT + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'FUNCTION' AS object_type, + p.proacl AS acl + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'f' + + UNION ALL + + -- Procedures + SELECT + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'PROCEDURE' AS object_type, + p.proacl AS acl + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'p' + + UNION ALL + + -- Types + SELECT + t.typname AS object_name, + 'TYPE' AS object_type, + t.typacl AS acl + FROM pg_type t + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE n.nspname = $1 + AND t.typtype IN ('e', 'c', 'd') +), +public_grants AS ( + SELECT + object_name, + object_type, + EXISTS ( + SELECT 1 + FROM unnest(acl) AS acl_entry + WHERE acl_entry::text LIKE '=%' -- PUBLIC grants start with = + ) AS has_public_grant, + acl IS NOT NULL AS has_explicit_acl + FROM objects_with_acl +) +SELECT object_name, object_type +FROM public_grants +WHERE has_explicit_acl = true AND has_public_grant = false +ORDER BY object_type, object_name; \ No newline at end of file diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 9978a80c..1d6a6e52 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -1846,6 +1846,125 @@ func (q *Queries) GetPartitionedTablesForSchema(ctx context.Context, dollar_1 sq return items, nil } +const getPrivilegesForSchema = `-- name: GetPrivilegesForSchema :many +WITH acl_data AS ( + -- Tables and Views + SELECT + n.nspname AS schema_name, + c.relname AS object_name, + CASE c.relkind + WHEN 'r' THEN 'TABLE' + WHEN 'v' THEN 'VIEW' + WHEN 'm' THEN 'VIEW' + WHEN 'S' THEN 'SEQUENCE' + END AS object_type, + c.relacl AS acl, + pg_get_userbyid(c.relowner) AS owner + FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE n.nspname = $1 + AND c.relkind IN ('r', 'v', 'm', 'S') + AND c.relacl IS NOT NULL + + UNION ALL + + -- Functions + SELECT + n.nspname AS schema_name, + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'FUNCTION' AS object_type, + p.proacl AS acl, + pg_get_userbyid(p.proowner) AS owner + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'f' + AND p.proacl IS NOT NULL + + UNION ALL + + -- Procedures + SELECT + n.nspname AS schema_name, + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'PROCEDURE' AS object_type, + p.proacl AS acl, + pg_get_userbyid(p.proowner) AS owner + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'p' + AND p.proacl IS NOT NULL + + UNION ALL + + -- Types (ENUM, COMPOSITE, DOMAIN) + SELECT + n.nspname AS schema_name, + t.typname AS object_name, + 'TYPE' AS object_type, + t.typacl AS acl, + pg_get_userbyid(t.typowner) AS owner + FROM pg_type t + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE n.nspname = $1 + AND t.typtype IN ('e', 'c', 'd') + AND t.typacl IS NOT NULL +) +SELECT + schema_name, + object_name, + object_type, + (aclexplode(acl)).grantee AS grantee_oid, + (aclexplode(acl)).privilege_type AS privilege_type, + (aclexplode(acl)).is_grantable AS is_grantable, + owner +FROM acl_data +ORDER BY object_type, object_name, grantee_oid, privilege_type +` + +type GetPrivilegesForSchemaRow struct { + SchemaName sql.NullString `db:"schema_name" json:"schema_name"` + ObjectName sql.NullString `db:"object_name" json:"object_name"` + ObjectType sql.NullString `db:"object_type" json:"object_type"` + GranteeOid interface{} `db:"grantee_oid" json:"grantee_oid"` + PrivilegeType sql.NullString `db:"privilege_type" json:"privilege_type"` + IsGrantable sql.NullBool `db:"is_grantable" json:"is_grantable"` + Owner sql.NullString `db:"owner" json:"owner"` +} + +// GetPrivilegesForSchema retrieves explicit privilege grants for objects in a specific schema +func (q *Queries) GetPrivilegesForSchema(ctx context.Context, dollar_1 sql.NullString) ([]GetPrivilegesForSchemaRow, error) { + rows, err := q.db.QueryContext(ctx, getPrivilegesForSchema, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPrivilegesForSchemaRow + for rows.Next() { + var i GetPrivilegesForSchemaRow + if err := rows.Scan( + &i.SchemaName, + &i.ObjectName, + &i.ObjectType, + &i.GranteeOid, + &i.PrivilegeType, + &i.IsGrantable, + &i.Owner, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getProcedures = `-- name: GetProcedures :many SELECT r.routine_schema, @@ -2210,6 +2329,89 @@ func (q *Queries) GetRLSTablesForSchema(ctx context.Context, nspname string) ([] return items, nil } +const getRevokedDefaultPrivilegesForSchema = `-- name: GetRevokedDefaultPrivilegesForSchema :many +WITH objects_with_acl AS ( + -- Functions (ACL may be NULL for default permissions; filtering happens in public_grants CTE) + SELECT + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'FUNCTION' AS object_type, + p.proacl AS acl + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'f' + + UNION ALL + + -- Procedures + SELECT + p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name, + 'PROCEDURE' AS object_type, + p.proacl AS acl + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.prokind = 'p' + + UNION ALL + + -- Types + SELECT + t.typname AS object_name, + 'TYPE' AS object_type, + t.typacl AS acl + FROM pg_type t + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE n.nspname = $1 + AND t.typtype IN ('e', 'c', 'd') +), +public_grants AS ( + SELECT + object_name, + object_type, + EXISTS ( + SELECT 1 + FROM unnest(acl) AS acl_entry + WHERE acl_entry::text LIKE '=%' -- PUBLIC grants start with = + ) AS has_public_grant, + acl IS NOT NULL AS has_explicit_acl + FROM objects_with_acl +) +SELECT object_name, object_type +FROM public_grants +WHERE has_explicit_acl = true AND has_public_grant = false +ORDER BY object_type, object_name +` + +type GetRevokedDefaultPrivilegesForSchemaRow struct { + ObjectName sql.NullString `db:"object_name" json:"object_name"` + ObjectType sql.NullString `db:"object_type" json:"object_type"` +} + +// GetRevokedDefaultPrivilegesForSchema finds objects where default PUBLIC grants have been explicitly revoked +func (q *Queries) GetRevokedDefaultPrivilegesForSchema(ctx context.Context, dollar_1 sql.NullString) ([]GetRevokedDefaultPrivilegesForSchemaRow, error) { + rows, err := q.db.QueryContext(ctx, getRevokedDefaultPrivilegesForSchema, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetRevokedDefaultPrivilegesForSchemaRow + for rows.Next() { + var i GetRevokedDefaultPrivilegesForSchemaRow + if err := rows.Scan(&i.ObjectName, &i.ObjectType); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getSchema = `-- name: GetSchema :one SELECT schema_name diff --git a/testdata/diff/privilege/alter_privilege/diff.sql b/testdata/diff/privilege/alter_privilege/diff.sql new file mode 100644 index 00000000..57cef26b --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/diff.sql @@ -0,0 +1,3 @@ +REVOKE INSERT ON TABLE inventory FROM app_role; + +GRANT DELETE, UPDATE ON TABLE inventory TO app_role; diff --git a/testdata/diff/privilege/alter_privilege/new.sql b/testdata/diff/privilege/alter_privilege/new.sql new file mode 100644 index 00000000..2bd5edb5 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/new.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE TABLE inventory (id serial PRIMARY KEY); + +GRANT SELECT, UPDATE, DELETE ON inventory TO app_role; diff --git a/testdata/diff/privilege/alter_privilege/old.sql b/testdata/diff/privilege/alter_privilege/old.sql new file mode 100644 index 00000000..96987d19 --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/old.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE TABLE inventory (id serial PRIMARY KEY); + +GRANT SELECT, INSERT ON inventory TO app_role; diff --git a/testdata/diff/privilege/alter_privilege/plan.json b/testdata/diff/privilege/alter_privilege/plan.json new file mode 100644 index 00000000..05d5e2bf --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/plan.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "37a83f6528ef1727418deb17421226e68badb29f678e5ea7ba1a0947f51e220c" + }, + "groups": [ + { + "steps": [ + { + "sql": "REVOKE INSERT ON TABLE inventory FROM app_role;", + "type": "privilege", + "operation": "alter", + "path": "privileges.TABLE.inventory.app_role" + }, + { + "sql": "GRANT DELETE, UPDATE ON TABLE inventory TO app_role;", + "type": "privilege", + "operation": "alter", + "path": "privileges.TABLE.inventory.app_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/alter_privilege/plan.sql b/testdata/diff/privilege/alter_privilege/plan.sql new file mode 100644 index 00000000..57cef26b --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/plan.sql @@ -0,0 +1,3 @@ +REVOKE INSERT ON TABLE inventory FROM app_role; + +GRANT DELETE, UPDATE ON TABLE inventory TO app_role; diff --git a/testdata/diff/privilege/alter_privilege/plan.txt b/testdata/diff/privilege/alter_privilege/plan.txt new file mode 100644 index 00000000..07063cfc --- /dev/null +++ b/testdata/diff/privilege/alter_privilege/plan.txt @@ -0,0 +1,15 @@ +Plan: 2 to modify. + +Summary by type: + privileges: 2 to modify + +Privileges: + ~ app_role + ~ app_role + +DDL to be executed: +-------------------------------------------------- + +REVOKE INSERT ON TABLE inventory FROM app_role; + +GRANT DELETE, UPDATE ON TABLE inventory TO app_role; diff --git a/testdata/diff/privilege/grant_function_execute/diff.sql b/testdata/diff/privilege/grant_function_execute/diff.sql new file mode 100644 index 00000000..9ced8294 --- /dev/null +++ b/testdata/diff/privilege/grant_function_execute/diff.sql @@ -0,0 +1 @@ +GRANT EXECUTE ON FUNCTION calculate_total(quantity integer, unit_price numeric) TO api_role; diff --git a/testdata/diff/privilege/grant_function_execute/new.sql b/testdata/diff/privilege/grant_function_execute/new.sql new file mode 100644 index 00000000..110e7ea5 --- /dev/null +++ b/testdata/diff/privilege/grant_function_execute/new.sql @@ -0,0 +1,13 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'api_role') THEN + CREATE ROLE api_role; + END IF; +END $$; + +CREATE FUNCTION calculate_total(quantity integer, unit_price numeric) +RETURNS numeric +LANGUAGE sql +AS $$ SELECT quantity * unit_price; $$; + +GRANT EXECUTE ON FUNCTION calculate_total(integer, numeric) TO api_role; diff --git a/testdata/diff/privilege/grant_function_execute/old.sql b/testdata/diff/privilege/grant_function_execute/old.sql new file mode 100644 index 00000000..d0b4fc55 --- /dev/null +++ b/testdata/diff/privilege/grant_function_execute/old.sql @@ -0,0 +1,11 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'api_role') THEN + CREATE ROLE api_role; + END IF; +END $$; + +CREATE FUNCTION calculate_total(quantity integer, unit_price numeric) +RETURNS numeric +LANGUAGE sql +AS $$ SELECT quantity * unit_price; $$; diff --git a/testdata/diff/privilege/grant_function_execute/plan.json b/testdata/diff/privilege/grant_function_execute/plan.json new file mode 100644 index 00000000..cac3ba8d --- /dev/null +++ b/testdata/diff/privilege/grant_function_execute/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "ccf790899245f18bb779a82938ba62c137eb1a3807644be4a278b8e999a8fe30" + }, + "groups": [ + { + "steps": [ + { + "sql": "GRANT EXECUTE ON FUNCTION calculate_total(quantity integer, unit_price numeric) TO api_role;", + "type": "privilege", + "operation": "create", + "path": "privileges.FUNCTION.calculate_total(quantity integer, unit_price numeric).api_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/grant_function_execute/plan.sql b/testdata/diff/privilege/grant_function_execute/plan.sql new file mode 100644 index 00000000..9ced8294 --- /dev/null +++ b/testdata/diff/privilege/grant_function_execute/plan.sql @@ -0,0 +1 @@ +GRANT EXECUTE ON FUNCTION calculate_total(quantity integer, unit_price numeric) TO api_role; diff --git a/testdata/diff/privilege/grant_function_execute/plan.txt b/testdata/diff/privilege/grant_function_execute/plan.txt new file mode 100644 index 00000000..86fcf79f --- /dev/null +++ b/testdata/diff/privilege/grant_function_execute/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + privileges: 1 to add + +Privileges: + + api_role + +DDL to be executed: +-------------------------------------------------- + +GRANT EXECUTE ON FUNCTION calculate_total(quantity integer, unit_price numeric) TO api_role; diff --git a/testdata/diff/privilege/grant_sequence/diff.sql b/testdata/diff/privilege/grant_sequence/diff.sql new file mode 100644 index 00000000..516da426 --- /dev/null +++ b/testdata/diff/privilege/grant_sequence/diff.sql @@ -0,0 +1 @@ +GRANT SELECT, USAGE ON SEQUENCE order_id_seq TO app_role; diff --git a/testdata/diff/privilege/grant_sequence/new.sql b/testdata/diff/privilege/grant_sequence/new.sql new file mode 100644 index 00000000..df5b26fb --- /dev/null +++ b/testdata/diff/privilege/grant_sequence/new.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE SEQUENCE order_id_seq; + +GRANT USAGE, SELECT ON SEQUENCE order_id_seq TO app_role; diff --git a/testdata/diff/privilege/grant_sequence/old.sql b/testdata/diff/privilege/grant_sequence/old.sql new file mode 100644 index 00000000..33e42312 --- /dev/null +++ b/testdata/diff/privilege/grant_sequence/old.sql @@ -0,0 +1,8 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE SEQUENCE order_id_seq; diff --git a/testdata/diff/privilege/grant_sequence/plan.json b/testdata/diff/privilege/grant_sequence/plan.json new file mode 100644 index 00000000..fe94f0c9 --- /dev/null +++ b/testdata/diff/privilege/grant_sequence/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "7027e39d7e59e0103d597e7cd942af1e42217adff9a2c893a46d9b16c6a2ee25" + }, + "groups": [ + { + "steps": [ + { + "sql": "GRANT SELECT, USAGE ON SEQUENCE order_id_seq TO app_role;", + "type": "privilege", + "operation": "create", + "path": "privileges.SEQUENCE.order_id_seq.app_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/grant_sequence/plan.sql b/testdata/diff/privilege/grant_sequence/plan.sql new file mode 100644 index 00000000..516da426 --- /dev/null +++ b/testdata/diff/privilege/grant_sequence/plan.sql @@ -0,0 +1 @@ +GRANT SELECT, USAGE ON SEQUENCE order_id_seq TO app_role; diff --git a/testdata/diff/privilege/grant_sequence/plan.txt b/testdata/diff/privilege/grant_sequence/plan.txt new file mode 100644 index 00000000..989e8e07 --- /dev/null +++ b/testdata/diff/privilege/grant_sequence/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + privileges: 1 to add + +Privileges: + + app_role + +DDL to be executed: +-------------------------------------------------- + +GRANT SELECT, USAGE ON SEQUENCE order_id_seq TO app_role; diff --git a/testdata/diff/privilege/grant_table_multiple/diff.sql b/testdata/diff/privilege/grant_table_multiple/diff.sql new file mode 100644 index 00000000..fa14b710 --- /dev/null +++ b/testdata/diff/privilege/grant_table_multiple/diff.sql @@ -0,0 +1 @@ +GRANT DELETE, INSERT, SELECT, UPDATE ON TABLE orders TO app_role; diff --git a/testdata/diff/privilege/grant_table_multiple/new.sql b/testdata/diff/privilege/grant_table_multiple/new.sql new file mode 100644 index 00000000..437fc426 --- /dev/null +++ b/testdata/diff/privilege/grant_table_multiple/new.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE TABLE orders (id serial PRIMARY KEY); + +GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_role; diff --git a/testdata/diff/privilege/grant_table_multiple/old.sql b/testdata/diff/privilege/grant_table_multiple/old.sql new file mode 100644 index 00000000..b3951b6b --- /dev/null +++ b/testdata/diff/privilege/grant_table_multiple/old.sql @@ -0,0 +1,8 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE TABLE orders (id serial PRIMARY KEY); diff --git a/testdata/diff/privilege/grant_table_multiple/plan.json b/testdata/diff/privilege/grant_table_multiple/plan.json new file mode 100644 index 00000000..5090b2d3 --- /dev/null +++ b/testdata/diff/privilege/grant_table_multiple/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "b58434507a24c7e16d958486d79ce94453f9a249af1f3f4bc2f4d3479d22ae0d" + }, + "groups": [ + { + "steps": [ + { + "sql": "GRANT DELETE, INSERT, SELECT, UPDATE ON TABLE orders TO app_role;", + "type": "privilege", + "operation": "create", + "path": "privileges.TABLE.orders.app_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/grant_table_multiple/plan.sql b/testdata/diff/privilege/grant_table_multiple/plan.sql new file mode 100644 index 00000000..fa14b710 --- /dev/null +++ b/testdata/diff/privilege/grant_table_multiple/plan.sql @@ -0,0 +1 @@ +GRANT DELETE, INSERT, SELECT, UPDATE ON TABLE orders TO app_role; diff --git a/testdata/diff/privilege/grant_table_multiple/plan.txt b/testdata/diff/privilege/grant_table_multiple/plan.txt new file mode 100644 index 00000000..d0833240 --- /dev/null +++ b/testdata/diff/privilege/grant_table_multiple/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + privileges: 1 to add + +Privileges: + + app_role + +DDL to be executed: +-------------------------------------------------- + +GRANT DELETE, INSERT, SELECT, UPDATE ON TABLE orders TO app_role; diff --git a/testdata/diff/privilege/grant_table_select/diff.sql b/testdata/diff/privilege/grant_table_select/diff.sql new file mode 100644 index 00000000..b3a2836a --- /dev/null +++ b/testdata/diff/privilege/grant_table_select/diff.sql @@ -0,0 +1 @@ +GRANT SELECT ON TABLE users TO readonly_role; diff --git a/testdata/diff/privilege/grant_table_select/new.sql b/testdata/diff/privilege/grant_table_select/new.sql new file mode 100644 index 00000000..2dfed5bf --- /dev/null +++ b/testdata/diff/privilege/grant_table_select/new.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'readonly_role') THEN + CREATE ROLE readonly_role; + END IF; +END $$; + +CREATE TABLE users (id serial PRIMARY KEY); + +GRANT SELECT ON users TO readonly_role; diff --git a/testdata/diff/privilege/grant_table_select/old.sql b/testdata/diff/privilege/grant_table_select/old.sql new file mode 100644 index 00000000..831b850d --- /dev/null +++ b/testdata/diff/privilege/grant_table_select/old.sql @@ -0,0 +1,8 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'readonly_role') THEN + CREATE ROLE readonly_role; + END IF; +END $$; + +CREATE TABLE users (id serial PRIMARY KEY); diff --git a/testdata/diff/privilege/grant_table_select/plan.json b/testdata/diff/privilege/grant_table_select/plan.json new file mode 100644 index 00000000..e1073b2b --- /dev/null +++ b/testdata/diff/privilege/grant_table_select/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "930412dd8d3510344c8498a09f757a5e375ae4d2e101eae2c14808fea7dd3594" + }, + "groups": [ + { + "steps": [ + { + "sql": "GRANT SELECT ON TABLE users TO readonly_role;", + "type": "privilege", + "operation": "create", + "path": "privileges.TABLE.users.readonly_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/grant_table_select/plan.sql b/testdata/diff/privilege/grant_table_select/plan.sql new file mode 100644 index 00000000..b3a2836a --- /dev/null +++ b/testdata/diff/privilege/grant_table_select/plan.sql @@ -0,0 +1 @@ +GRANT SELECT ON TABLE users TO readonly_role; diff --git a/testdata/diff/privilege/grant_table_select/plan.txt b/testdata/diff/privilege/grant_table_select/plan.txt new file mode 100644 index 00000000..8e27cf46 --- /dev/null +++ b/testdata/diff/privilege/grant_table_select/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + privileges: 1 to add + +Privileges: + + readonly_role + +DDL to be executed: +-------------------------------------------------- + +GRANT SELECT ON TABLE users TO readonly_role; diff --git a/testdata/diff/privilege/grant_type/diff.sql b/testdata/diff/privilege/grant_type/diff.sql new file mode 100644 index 00000000..fe3d1458 --- /dev/null +++ b/testdata/diff/privilege/grant_type/diff.sql @@ -0,0 +1 @@ +GRANT USAGE ON TYPE email_address TO app_role; diff --git a/testdata/diff/privilege/grant_type/new.sql b/testdata/diff/privilege/grant_type/new.sql new file mode 100644 index 00000000..82c5efa2 --- /dev/null +++ b/testdata/diff/privilege/grant_type/new.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE DOMAIN email_address AS text; + +GRANT USAGE ON TYPE email_address TO app_role; diff --git a/testdata/diff/privilege/grant_type/old.sql b/testdata/diff/privilege/grant_type/old.sql new file mode 100644 index 00000000..b1524ea1 --- /dev/null +++ b/testdata/diff/privilege/grant_type/old.sql @@ -0,0 +1,8 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_role') THEN + CREATE ROLE app_role; + END IF; +END $$; + +CREATE DOMAIN email_address AS text; diff --git a/testdata/diff/privilege/grant_type/plan.json b/testdata/diff/privilege/grant_type/plan.json new file mode 100644 index 00000000..e47f4c09 --- /dev/null +++ b/testdata/diff/privilege/grant_type/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "23c1e89c461f82f0fe9f07f1090fd3a9ca0da2300fb48a6f9300b688f2eb31b8" + }, + "groups": [ + { + "steps": [ + { + "sql": "GRANT USAGE ON TYPE email_address TO app_role;", + "type": "privilege", + "operation": "create", + "path": "privileges.TYPE.email_address.app_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/grant_type/plan.sql b/testdata/diff/privilege/grant_type/plan.sql new file mode 100644 index 00000000..fe3d1458 --- /dev/null +++ b/testdata/diff/privilege/grant_type/plan.sql @@ -0,0 +1 @@ +GRANT USAGE ON TYPE email_address TO app_role; diff --git a/testdata/diff/privilege/grant_type/plan.txt b/testdata/diff/privilege/grant_type/plan.txt new file mode 100644 index 00000000..5f753e54 --- /dev/null +++ b/testdata/diff/privilege/grant_type/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + privileges: 1 to add + +Privileges: + + app_role + +DDL to be executed: +-------------------------------------------------- + +GRANT USAGE ON TYPE email_address TO app_role; diff --git a/testdata/diff/privilege/grant_with_grant_option/diff.sql b/testdata/diff/privilege/grant_with_grant_option/diff.sql new file mode 100644 index 00000000..923cd685 --- /dev/null +++ b/testdata/diff/privilege/grant_with_grant_option/diff.sql @@ -0,0 +1 @@ +GRANT SELECT ON TABLE products TO admin_role WITH GRANT OPTION; diff --git a/testdata/diff/privilege/grant_with_grant_option/new.sql b/testdata/diff/privilege/grant_with_grant_option/new.sql new file mode 100644 index 00000000..b6985d69 --- /dev/null +++ b/testdata/diff/privilege/grant_with_grant_option/new.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'admin_role') THEN + CREATE ROLE admin_role; + END IF; +END $$; + +CREATE TABLE products (id serial PRIMARY KEY); + +GRANT SELECT ON products TO admin_role WITH GRANT OPTION; diff --git a/testdata/diff/privilege/grant_with_grant_option/old.sql b/testdata/diff/privilege/grant_with_grant_option/old.sql new file mode 100644 index 00000000..6ce9380a --- /dev/null +++ b/testdata/diff/privilege/grant_with_grant_option/old.sql @@ -0,0 +1,8 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'admin_role') THEN + CREATE ROLE admin_role; + END IF; +END $$; + +CREATE TABLE products (id serial PRIMARY KEY); diff --git a/testdata/diff/privilege/grant_with_grant_option/plan.json b/testdata/diff/privilege/grant_with_grant_option/plan.json new file mode 100644 index 00000000..1f1ca34c --- /dev/null +++ b/testdata/diff/privilege/grant_with_grant_option/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "1cc2abbffadb0f96d897c60ddc96681b9a3d629a25e905e5ca208d98cd4d7a22" + }, + "groups": [ + { + "steps": [ + { + "sql": "GRANT SELECT ON TABLE products TO admin_role WITH GRANT OPTION;", + "type": "privilege", + "operation": "create", + "path": "privileges.TABLE.products.admin_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/grant_with_grant_option/plan.sql b/testdata/diff/privilege/grant_with_grant_option/plan.sql new file mode 100644 index 00000000..923cd685 --- /dev/null +++ b/testdata/diff/privilege/grant_with_grant_option/plan.sql @@ -0,0 +1 @@ +GRANT SELECT ON TABLE products TO admin_role WITH GRANT OPTION; diff --git a/testdata/diff/privilege/grant_with_grant_option/plan.txt b/testdata/diff/privilege/grant_with_grant_option/plan.txt new file mode 100644 index 00000000..f1e315a5 --- /dev/null +++ b/testdata/diff/privilege/grant_with_grant_option/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + privileges: 1 to add + +Privileges: + + admin_role + +DDL to be executed: +-------------------------------------------------- + +GRANT SELECT ON TABLE products TO admin_role WITH GRANT OPTION; diff --git a/testdata/diff/privilege/revoke_grant_option/diff.sql b/testdata/diff/privilege/revoke_grant_option/diff.sql new file mode 100644 index 00000000..bfbc6435 --- /dev/null +++ b/testdata/diff/privilege/revoke_grant_option/diff.sql @@ -0,0 +1 @@ +REVOKE GRANT OPTION FOR SELECT ON TABLE employees FROM manager_role; diff --git a/testdata/diff/privilege/revoke_grant_option/new.sql b/testdata/diff/privilege/revoke_grant_option/new.sql new file mode 100644 index 00000000..23cf899d --- /dev/null +++ b/testdata/diff/privilege/revoke_grant_option/new.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'manager_role') THEN + CREATE ROLE manager_role; + END IF; +END $$; + +CREATE TABLE employees (id serial PRIMARY KEY); + +GRANT SELECT ON employees TO manager_role; diff --git a/testdata/diff/privilege/revoke_grant_option/old.sql b/testdata/diff/privilege/revoke_grant_option/old.sql new file mode 100644 index 00000000..eacecb61 --- /dev/null +++ b/testdata/diff/privilege/revoke_grant_option/old.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'manager_role') THEN + CREATE ROLE manager_role; + END IF; +END $$; + +CREATE TABLE employees (id serial PRIMARY KEY); + +GRANT SELECT ON employees TO manager_role WITH GRANT OPTION; diff --git a/testdata/diff/privilege/revoke_grant_option/plan.json b/testdata/diff/privilege/revoke_grant_option/plan.json new file mode 100644 index 00000000..42572fe0 --- /dev/null +++ b/testdata/diff/privilege/revoke_grant_option/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "64ecb0bdec360cdb69238817cc218b0efe5a06b2c34c56e922b0beb2cafbd45c" + }, + "groups": [ + { + "steps": [ + { + "sql": "REVOKE GRANT OPTION FOR SELECT ON TABLE employees FROM manager_role;", + "type": "privilege", + "operation": "alter", + "path": "privileges.TABLE.employees.manager_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/revoke_grant_option/plan.sql b/testdata/diff/privilege/revoke_grant_option/plan.sql new file mode 100644 index 00000000..bfbc6435 --- /dev/null +++ b/testdata/diff/privilege/revoke_grant_option/plan.sql @@ -0,0 +1 @@ +REVOKE GRANT OPTION FOR SELECT ON TABLE employees FROM manager_role; diff --git a/testdata/diff/privilege/revoke_grant_option/plan.txt b/testdata/diff/privilege/revoke_grant_option/plan.txt new file mode 100644 index 00000000..c0b27f90 --- /dev/null +++ b/testdata/diff/privilege/revoke_grant_option/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to modify. + +Summary by type: + privileges: 1 to modify + +Privileges: + ~ manager_role + +DDL to be executed: +-------------------------------------------------- + +REVOKE GRANT OPTION FOR SELECT ON TABLE employees FROM manager_role; diff --git a/testdata/diff/privilege/revoke_public_function/diff.sql b/testdata/diff/privilege/revoke_public_function/diff.sql new file mode 100644 index 00000000..9178d1b5 --- /dev/null +++ b/testdata/diff/privilege/revoke_public_function/diff.sql @@ -0,0 +1 @@ +REVOKE EXECUTE ON FUNCTION get_user_data(user_id integer) FROM PUBLIC; diff --git a/testdata/diff/privilege/revoke_public_function/new.sql b/testdata/diff/privilege/revoke_public_function/new.sql new file mode 100644 index 00000000..32b25460 --- /dev/null +++ b/testdata/diff/privilege/revoke_public_function/new.sql @@ -0,0 +1,6 @@ +CREATE FUNCTION get_user_data(user_id integer) +RETURNS text +LANGUAGE sql +AS $$ SELECT 'user_' || user_id::text; $$; + +REVOKE EXECUTE ON FUNCTION get_user_data(integer) FROM PUBLIC; diff --git a/testdata/diff/privilege/revoke_public_function/old.sql b/testdata/diff/privilege/revoke_public_function/old.sql new file mode 100644 index 00000000..a964ec06 --- /dev/null +++ b/testdata/diff/privilege/revoke_public_function/old.sql @@ -0,0 +1,4 @@ +CREATE FUNCTION get_user_data(user_id integer) +RETURNS text +LANGUAGE sql +AS $$ SELECT 'user_' || user_id::text; $$; diff --git a/testdata/diff/privilege/revoke_public_function/plan.json b/testdata/diff/privilege/revoke_public_function/plan.json new file mode 100644 index 00000000..478c7dde --- /dev/null +++ b/testdata/diff/privilege/revoke_public_function/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "8f430acd242ac0e21c80ea77aaab0b48ad04a543aeb04dee3c19ac2c89f5dd8c" + }, + "groups": [ + { + "steps": [ + { + "sql": "REVOKE EXECUTE ON FUNCTION get_user_data(user_id integer) FROM PUBLIC;", + "type": "revoked_default_privilege", + "operation": "create", + "path": "revoked_default.FUNCTION.get_user_data(user_id integer)" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/revoke_public_function/plan.sql b/testdata/diff/privilege/revoke_public_function/plan.sql new file mode 100644 index 00000000..9178d1b5 --- /dev/null +++ b/testdata/diff/privilege/revoke_public_function/plan.sql @@ -0,0 +1 @@ +REVOKE EXECUTE ON FUNCTION get_user_data(user_id integer) FROM PUBLIC; diff --git a/testdata/diff/privilege/revoke_public_function/plan.txt b/testdata/diff/privilege/revoke_public_function/plan.txt new file mode 100644 index 00000000..a3cb3b54 --- /dev/null +++ b/testdata/diff/privilege/revoke_public_function/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to add. + +Summary by type: + revoked default privileges: 1 to add + +Revoked default privileges: + + get_user_data(user_id integer) + +DDL to be executed: +-------------------------------------------------- + +REVOKE EXECUTE ON FUNCTION get_user_data(user_id integer) FROM PUBLIC; diff --git a/testdata/diff/privilege/revoke_table_privilege/diff.sql b/testdata/diff/privilege/revoke_table_privilege/diff.sql new file mode 100644 index 00000000..e5be6057 --- /dev/null +++ b/testdata/diff/privilege/revoke_table_privilege/diff.sql @@ -0,0 +1 @@ +REVOKE SELECT ON TABLE audit_log FROM old_role; diff --git a/testdata/diff/privilege/revoke_table_privilege/new.sql b/testdata/diff/privilege/revoke_table_privilege/new.sql new file mode 100644 index 00000000..77113fff --- /dev/null +++ b/testdata/diff/privilege/revoke_table_privilege/new.sql @@ -0,0 +1,8 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'old_role') THEN + CREATE ROLE old_role; + END IF; +END $$; + +CREATE TABLE audit_log (id serial PRIMARY KEY); diff --git a/testdata/diff/privilege/revoke_table_privilege/old.sql b/testdata/diff/privilege/revoke_table_privilege/old.sql new file mode 100644 index 00000000..a02267bc --- /dev/null +++ b/testdata/diff/privilege/revoke_table_privilege/old.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'old_role') THEN + CREATE ROLE old_role; + END IF; +END $$; + +CREATE TABLE audit_log (id serial PRIMARY KEY); + +GRANT SELECT ON audit_log TO old_role; diff --git a/testdata/diff/privilege/revoke_table_privilege/plan.json b/testdata/diff/privilege/revoke_table_privilege/plan.json new file mode 100644 index 00000000..45c752d5 --- /dev/null +++ b/testdata/diff/privilege/revoke_table_privilege/plan.json @@ -0,0 +1,20 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.5.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "98ec3c4723e9dcc7194e1ecdf00bc3c1b7a54a1cbe638a8e2531da763900580e" + }, + "groups": [ + { + "steps": [ + { + "sql": "REVOKE SELECT ON TABLE audit_log FROM old_role;", + "type": "privilege", + "operation": "drop", + "path": "privileges.TABLE.audit_log.old_role" + } + ] + } + ] +} diff --git a/testdata/diff/privilege/revoke_table_privilege/plan.sql b/testdata/diff/privilege/revoke_table_privilege/plan.sql new file mode 100644 index 00000000..e5be6057 --- /dev/null +++ b/testdata/diff/privilege/revoke_table_privilege/plan.sql @@ -0,0 +1 @@ +REVOKE SELECT ON TABLE audit_log FROM old_role; diff --git a/testdata/diff/privilege/revoke_table_privilege/plan.txt b/testdata/diff/privilege/revoke_table_privilege/plan.txt new file mode 100644 index 00000000..fd400c13 --- /dev/null +++ b/testdata/diff/privilege/revoke_table_privilege/plan.txt @@ -0,0 +1,12 @@ +Plan: 1 to drop. + +Summary by type: + privileges: 1 to drop + +Privileges: + - old_role + +DDL to be executed: +-------------------------------------------------- + +REVOKE SELECT ON TABLE audit_log FROM old_role;