@@ -14,6 +14,9 @@ import (
1414 "go.uber.org/zap"
1515)
1616
17+ var errRevokeGrantsFromRole = errors .New ("error revoking grants from role" )
18+ var errRevokeParentRolesFromRole = errors .New ("error revoking parent roles from role" )
19+
1720type RoleModel struct {
1821 ID int64 `db:"oid"`
1922 Name string `db:"rolname"`
@@ -157,19 +160,268 @@ func (c *Client) CreateRole(ctx context.Context, roleName string) error {
157160 return err
158161}
159162
160- func (c * Client ) DeleteRole (ctx context.Context , roleName string ) error {
163+ // RoleOwnsObjects checks if a role owns any database objects.
164+ func (c * Client ) RoleOwnsObjects (ctx context.Context , roleName string ) (bool , error ) {
165+ l := ctxzap .Extract (ctx )
166+
167+ query := `
168+ SELECT EXISTS(
169+ SELECT 1 FROM (
170+ -- Check for owned schemas
171+ SELECT 1 FROM pg_namespace WHERE nspowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
172+ UNION ALL
173+ -- Check for owned tables
174+ SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
175+ UNION ALL
176+ -- Check for owned functions
177+ SELECT 1 FROM pg_proc WHERE proowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
178+ UNION ALL
179+ -- Check for owned sequences
180+ SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1) AND relkind = 'S'
181+ UNION ALL
182+ -- Check for owned views
183+ SELECT 1 FROM pg_class WHERE relowner = (SELECT oid FROM pg_roles WHERE rolname = $1) AND relkind = 'v'
184+ UNION ALL
185+ -- Check for owned types
186+ SELECT 1 FROM pg_type WHERE typowner = (SELECT oid FROM pg_roles WHERE rolname = $1)
187+ UNION ALL
188+ -- Check for owned databases
189+ SELECT 1 FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1)
190+ ) owned_objects
191+ )`
192+
193+ var ownsObjects bool
194+ err := c .db .QueryRow (ctx , query , roleName ).Scan (& ownsObjects )
195+ if err != nil {
196+ l .Error ("error checking if role owns objects" , zap .Error (err ))
197+ return false , err
198+ }
199+
200+ return ownsObjects , nil
201+ }
202+
203+ // RevokeAllGrantsFromRole revokes all grants from a role across all schemas.
204+ func (c * Client ) RevokeAllGrantsFromRole (ctx context.Context , roleName string ) error {
205+ l := ctxzap .Extract (ctx )
206+
207+ sanitizedRoleName := pgx.Identifier {roleName }.Sanitize ()
208+
209+ schemasQuery := `
210+ SELECT nspname
211+ FROM pg_namespace
212+ WHERE nspname NOT LIKE 'pg_%'
213+ AND nspname != 'information_schema'
214+ ORDER BY nspname`
215+
216+ rows , err := c .db .Query (ctx , schemasQuery )
217+ if err != nil {
218+ l .Error ("error querying schemas" , zap .Error (err ))
219+ return err
220+ }
221+ defer rows .Close ()
222+
223+ var schemas []string
224+ for rows .Next () {
225+ var schemaName string
226+ if err := rows .Scan (& schemaName ); err != nil {
227+ l .Error ("error scanning schema name" , zap .Error (err ))
228+ return err
229+ }
230+ schemas = append (schemas , schemaName )
231+ }
232+
233+ if err := rows .Err (); err != nil {
234+ l .Error ("error iterating schemas" , zap .Error (err ))
235+ return err
236+ }
237+
238+ var revokeError error
239+ for _ , schema := range schemas {
240+ sanitizedSchema := pgx.Identifier {schema }.Sanitize ()
241+
242+ revokeTablesQuery := fmt .Sprintf ("REVOKE ALL ON ALL TABLES IN SCHEMA %s FROM %s" , sanitizedSchema , sanitizedRoleName )
243+ l .Debug ("revoking table grants" , zap .String ("query" , revokeTablesQuery ))
244+ if _ , err := c .db .Exec (ctx , revokeTablesQuery ); err != nil {
245+ l .Warn ("error revoking table grants" , zap .String ("schema" , schema ), zap .Error (err ))
246+ revokeError = errors .Join (revokeError , err )
247+ }
248+
249+ revokeSequencesQuery := fmt .Sprintf ("REVOKE ALL ON ALL SEQUENCES IN SCHEMA %s FROM %s" , sanitizedSchema , sanitizedRoleName )
250+ l .Debug ("revoking sequence grants" , zap .String ("query" , revokeSequencesQuery ))
251+ if _ , err := c .db .Exec (ctx , revokeSequencesQuery ); err != nil {
252+ l .Warn ("error revoking sequence grants" , zap .String ("schema" , schema ), zap .Error (err ))
253+ revokeError = errors .Join (revokeError , err )
254+ }
255+
256+ revokeFunctionsQuery := fmt .Sprintf ("REVOKE ALL ON ALL FUNCTIONS IN SCHEMA %s FROM %s" , sanitizedSchema , sanitizedRoleName )
257+ l .Debug ("revoking function grants" , zap .String ("query" , revokeFunctionsQuery ))
258+ if _ , err := c .db .Exec (ctx , revokeFunctionsQuery ); err != nil {
259+ l .Warn ("error revoking function grants" , zap .String ("schema" , schema ), zap .Error (err ))
260+ revokeError = errors .Join (revokeError , err )
261+ }
262+
263+ typesQuery := `
264+ SELECT typname
265+ FROM pg_type t
266+ JOIN pg_namespace n ON t.typnamespace = n.oid
267+ WHERE n.nspname = $1
268+ AND t.typtype = 'c'`
269+
270+ typeRows , err := c .db .Query (ctx , typesQuery , schema )
271+ if err != nil {
272+ l .Warn ("error querying types" , zap .String ("schema" , schema ), zap .Error (err ))
273+ revokeError = errors .Join (revokeError , err )
274+ } else {
275+ defer typeRows .Close ()
276+
277+ for typeRows .Next () {
278+ var typeName string
279+ if err := typeRows .Scan (& typeName ); err != nil {
280+ l .Warn ("error scanning type name" , zap .String ("schema" , schema ), zap .Error (err ))
281+ revokeError = errors .Join (revokeError , err )
282+ continue
283+ }
284+
285+ sanitizedTypeName := pgx.Identifier {schema , typeName }.Sanitize ()
286+ revokeTypeQuery := fmt .Sprintf ("REVOKE ALL ON TYPE %s FROM %s" , sanitizedTypeName , sanitizedRoleName )
287+ l .Debug ("revoking type grants" , zap .String ("query" , revokeTypeQuery ))
288+ if _ , err := c .db .Exec (ctx , revokeTypeQuery ); err != nil {
289+ l .Warn ("error revoking type grants" , zap .String ("schema" , schema ), zap .String ("type" , typeName ), zap .Error (err ))
290+ revokeError = errors .Join (revokeError , err )
291+ }
292+ }
293+ }
294+
295+ revokeSchemaQuery := fmt .Sprintf ("REVOKE ALL ON SCHEMA %s FROM %s" , sanitizedSchema , sanitizedRoleName )
296+ l .Debug ("revoking schema grants" , zap .String ("query" , revokeSchemaQuery ))
297+ if _ , err := c .db .Exec (ctx , revokeSchemaQuery ); err != nil {
298+ l .Warn ("error revoking schema grants" , zap .String ("schema" , schema ), zap .Error (err ))
299+ revokeError = errors .Join (revokeError , err )
300+ }
301+ }
302+
303+ revokeDbQuery := fmt .Sprintf ("REVOKE ALL ON DATABASE %s FROM %s" , pgx.Identifier {c .DatabaseName ()}.Sanitize (), sanitizedRoleName )
304+ l .Debug ("revoking database grants" , zap .String ("query" , revokeDbQuery ))
305+ if _ , err := c .db .Exec (ctx , revokeDbQuery ); err != nil {
306+ l .Warn ("error revoking database grants" , zap .Error (err ))
307+ revokeError = errors .Join (revokeError , err )
308+ }
309+
310+ if revokeError != nil {
311+ return errors .Join (errRevokeGrantsFromRole , revokeError )
312+ }
313+
314+ return nil
315+ }
316+
317+ // RemoveRoleFromAllRoles removes a role from all other roles.
318+ func (c * Client ) RemoveRoleFromAllRoles (ctx context.Context , roleName string ) error {
319+ l := ctxzap .Extract (ctx )
320+
321+ sanitizedRoleName := pgx.Identifier {roleName }.Sanitize ()
322+
323+ // Get all roles that have this role as a member
324+ query := `
325+ SELECT r.rolname
326+ FROM pg_roles r
327+ JOIN pg_auth_members am ON r.oid = am.roleid
328+ JOIN pg_roles member ON am.member = member.oid
329+ WHERE member.rolname = $1`
330+
331+ rows , err := c .db .Query (ctx , query , roleName )
332+ if err != nil {
333+ l .Error ("error querying role memberships" , zap .Error (err ))
334+ return err
335+ }
336+ defer rows .Close ()
337+
338+ var parentRoles []string
339+ for rows .Next () {
340+ var parentRole string
341+ if err := rows .Scan (& parentRole ); err != nil {
342+ l .Error ("error scanning parent role" , zap .Error (err ))
343+ return err
344+ }
345+ parentRoles = append (parentRoles , parentRole )
346+ }
347+
348+ if err := rows .Err (); err != nil {
349+ l .Error ("error iterating parent roles" , zap .Error (err ))
350+ return err
351+ }
352+
353+ var revokeError error
354+ // Remove the role from each parent role
355+ for _ , parentRole := range parentRoles {
356+ sanitizedParentRole := pgx.Identifier {parentRole }.Sanitize ()
357+ revokeQuery := fmt .Sprintf ("REVOKE %s FROM %s" , sanitizedParentRole , sanitizedRoleName )
358+
359+ l .Debug ("removing role from parent role" , zap .String ("query" , revokeQuery ))
360+ if _ , err := c .db .Exec (ctx , revokeQuery ); err != nil {
361+ l .Error ("error removing role from parent role" , zap .String ("parent_role" , parentRole ), zap .Error (err ))
362+ revokeError = errors .Join (revokeError , fmt .Errorf ("error removing role from %s role: %w" , parentRole , err ))
363+ }
364+ }
365+
366+ if revokeError != nil {
367+ return errors .Join (errRevokeParentRolesFromRole , revokeError )
368+ }
369+
370+ return nil
371+ }
372+
373+ // SafeDeleteRole safely deletes a role by first revoking grants and removing memberships.
374+ func (c * Client ) SafeDeleteRole (ctx context.Context , roleName string ) error {
161375 l := ctxzap .Extract (ctx )
162376
163377 if roleName == "" {
164378 return errors .New ("role name cannot be empty" )
165379 }
166380
381+ ownsObjects , err := c .RoleOwnsObjects (ctx , roleName )
382+ if err != nil {
383+ l .Error ("error checking if role owns objects" , zap .Error (err ))
384+ return err
385+ }
386+
387+ if ownsObjects {
388+ return fmt .Errorf ("cannot delete role '%s': role owns database objects (tables, schemas, functions, etc.). Please transfer ownership or drop objects first" , roleName )
389+ }
390+
391+ l .Debug ("revoking all grants from role" , zap .String ("role" , roleName ))
392+ grantsRevokeError := c .RevokeAllGrantsFromRole (ctx , roleName )
393+ if grantsRevokeError != nil {
394+ l .Error ("error revoking grants from role" , zap .Error (grantsRevokeError ))
395+ if ! errors .Is (grantsRevokeError , errRevokeGrantsFromRole ) {
396+ return fmt .Errorf ("error revoking existing grants from role: %w" , grantsRevokeError )
397+ }
398+ }
399+
400+ l .Debug ("removing role from all parent roles" , zap .String ("role" , roleName ))
401+ roleRevokeError := c .RemoveRoleFromAllRoles (ctx , roleName )
402+ if roleRevokeError != nil {
403+ l .Error ("error removing role from parent roles" , zap .Error (roleRevokeError ))
404+ if ! errors .Is (roleRevokeError , errRevokeParentRolesFromRole ) {
405+ return fmt .Errorf ("error removing role from parent roles: %w" , roleRevokeError )
406+ }
407+ }
408+
167409 sanitizedRoleName := pgx.Identifier {roleName }.Sanitize ()
168410 query := "DROP ROLE " + sanitizedRoleName
411+ l .Debug ("dropping role" , zap .String ("query" , query ))
412+ _ , err = c .db .Exec (ctx , query )
413+ if err != nil {
414+ l .Error ("error dropping role" , zap .Error (err ))
415+ finalError := errors .Join (err , roleRevokeError , grantsRevokeError )
416+ return fmt .Errorf ("error dropping role(%s): %w" , roleName , finalError )
417+ }
169418
170- l .Debug ("deleting role" , zap .String ("query" , query ))
171- _ , err := c .db .Exec (ctx , query )
172- return err
419+ l .Info ("successfully deleted role" , zap .String ("role" , roleName ))
420+ return nil
421+ }
422+
423+ func (c * Client ) DeleteRole (ctx context.Context , roleName string ) error {
424+ return c .SafeDeleteRole (ctx , roleName )
173425}
174426
175427func (c * Client ) CreateUser (ctx context.Context , login string , password string ) (* RoleModel , error ) {
0 commit comments