Support deleting users and groups via authctl#1372
Support deleting users and groups via authctl#1372shiv-tyagi wants to merge 5 commits intocanonical:mainfrom
Conversation
935182f to
2922411
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1372 +/- ##
==========================================
- Coverage 86.94% 86.56% -0.39%
==========================================
Files 93 95 +2
Lines 6366 6519 +153
Branches 111 111
==========================================
+ Hits 5535 5643 +108
- Misses 775 820 +45
Partials 56 56 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
nooreldeenmansour
left a comment
There was a problem hiding this comment.
Thanks for the PR! I did some experimentation and found a few issues worth addressing:
1. Home directory cleanup on user deletion
When a user is deleted, their home directory is left behind. We should either add a -r/--remove flag (similar to userdel ) to optionally clean it up, or at minimum make the warning message explicit that the home directory is preserved and requires manual cleanup.
2. Deleting a user's primary group should be blocked
We shouldn't allow deletion of a group that is the primary group of an existing user. This mirrors groupdel's behavior:
You may not remove the primary group of any existing user. You must remove the user before you remove the group.
It breaks the user's next login with: failed to update user "<username>": another system user exists with "<username>" name.
3. Deleting a user orphans their primary group
When a user is deleted, their primary group isn't cleaned up, leaving the database in an inconsistent state.
=== users query ===
<username>|10001|10001|<name>|/home/<username>
=== groups query ===
10000|<username>
The user can re-login, but is assigned a different GID 10001 (non-existent), while GID 10000 is orphaned. This also causes errors in id and groups:
<username>@authd-dev:~$ id
id: cannot find name for group ID 10001
uid=10001(<username>) gid=10001(10001) groups=10001(10001),24(cdrom),10000(<username>)
<username>@authd-dev:~$ groups
groups: cannot find name for group ID 10001
10001 cdrom <username>
4. Re-login after deletion assigns a new UID, breaking home directory ownership
If a deleted user logs back in and gets the same home directory, there is a chance they receive a different UID, meaning they will no longer own their home directory. checkHomeDirOwner detects UID/GID mismatch but only logs a warning in the jounal
|
Thanks for taking a look @nooreldeenmansour. I will work on closing #830 as well in the same PR. Regarding the issues you have highlighted,
Adding a flag to optionally delete the home directory makes sense.
Makes sense. I will take care of this.
I intentionally did it that way because I was unsure about the scenarios where some other user is also manually made part of another users primary group. I would like to hear your thoughts on how do we handle this situation? Should we implement it in a way that the user's primary group deletion happens only when only that user was part of it's group?
This is similar to the warning we show with the delete command. Once a user is deleted, its UID can be assigned to another user. Similarly when it is recreated, it might get a new UID. I think we can add the wordings for later in the warning message. Other than that, I couldn't figure out an easy way to handle that. Our warning message already suggests to lock user instead of deleting to make sure the UID stays reserved. Anything else you have in mind to handle this? |
I think it makes sense to handle this the standard UNIX way. As detailed in the userdel:
The warning message is likely sufficient, since it clearly states the UID drift as a potential consequence. I'll wait on @adombeck's suggestion regarding this |
That sounds reasonable. For the scenario where a user's primary group has other members as well - as far as I understand, authd can only determine whether some other user belongs to a users primary group if the other user itself is managed by authd (please correct me if that’s not accurate). Based on that, we could safely delete a user’s primary group when it has no other members, while also issuing a warning that any manually added users in that group would lose their group membership. @nooreldeenmansour @adombeck WDYT? P.S. I know you’re busy with the upcoming release, so feel free to respond whenever convenient. |
|
Thank you, @shiv-tyagi, for the PR and thank you @nooreldeenmansour for the excellent review!
I agree that we should handle the deletion of the broker's user data before merging this PR, because it would be confusing if we still kept user data after running
I agree that we should handle it the same way
I agree, a warning message explaining the issue with file ownership and suggesting to disable the user instead is the best we can do. |
We don't have to worry about that, because the primary groups only exist in the authd database, not |
|
Hi @adombeck @nooreldeenmansour
I tried implementing this in authd's User service. I am getting error deleting the home directory. When I checked I found that the authd's service is configured in a way that it cannot touch stuff inside Edit: the process would also require |
Also note that when targeting ubuntu we should also ensure that |
Yeah, we'll haven to loosen the restrictions on the authd systemd service (see |
Actually, after taking a closer look, I don't see anything in our existing service definition which would prevent writing to |
Which policies are you referring to, @3v1n0? |
That makes sense. Then let's add |
f3ef21f to
b6bf173
Compare
|
Hey @adombeck! I have submitted #1421 which contains the broker side of changes for user data deletion as suggested by you. This PR is also ready for another round of review. I have addressed the comments posted by @nooreldeenmansour. Please take a look and feel free to suggest improvements. |
05ddf87 to
18b33d8
Compare
|
|
Signed-off-by: Shiv Tyagi <shivtyagi3015@gmail.com>
Signed-off-by: Shiv Tyagi <shivtyagi3015@gmail.com>
…letion Signed-off-by: Shiv Tyagi <shivtyagi3015@gmail.com>
Signed-off-by: Shiv Tyagi <shivtyagi3015@gmail.com>
Signed-off-by: Shiv Tyagi <shivtyagi3015@gmail.com>
18b33d8 to
949d410
Compare
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for deleting authd-managed users and groups via authctl, including new gRPC APIs, DB/user-manager operations, broker-side cleanup hooks, and CLI confirmation/warnings.
Changes:
- Introduces
DeleteUser/DeleteGroupRPCs and implements server-side behavior (including broker cleanup for user deletion). - Adds user-manager + DB support for deleting users/groups, including guarding against deleting groups that are primary for existing users.
- Adds
authctl user delete/authctl group deletecommands with confirmation prompts,--yes, and--remove(user home) options, plus extensive golden/testdata updates.
Reviewed changes
Copilot reviewed 76 out of 76 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/users/manager.go | Implements Manager.DeleteUser/DeleteGroup and primary-group checks. |
| internal/users/manager_test.go | Adds unit tests for user/group deletion and primary-group detection. |
| internal/users/export_test.go | Exposes IsGroupPrimaryForUsers for tests. |
| internal/users/defs.go | Adds GroupIsPrimaryError surfaced through gRPC. |
| internal/users/db/users.go | Adds query to list users with a given primary GID. |
| internal/users/db/groups.go | Adds DB-level DeleteGroup implementation. |
| internal/users/db/db_test.go | Adds DB-level tests for DeleteGroup. |
| internal/users/db/testdata/golden/TestDeleteGroup/Deleting_sole_group_of_a_user_removes_group_and_memberships_but_keeps_user | Golden output for DB group deletion scenario. |
| internal/users/db/testdata/golden/TestDeleteGroup/Deleting_shared_group_removes_only_that_group_and_its_memberships | Golden output for DB shared-group deletion scenario. |
| internal/users/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml | Adds DB fixture with temp home paths for delete-user tests. |
| internal/users/testdata/db/multiple_users_and_groups_with_non_primary_group.db.yaml | Adds DB fixture for deleting a non-primary group. |
| internal/users/testdata/db/group_primary_check.db.yaml | Adds DB fixture for detecting primary-group users. |
| internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user | Golden output for delete-user behavior. |
| internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_and_remove_home | Golden output for delete-user with home removal. |
| internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_keeps_other_users_in_shared_group | Golden output ensuring shared group memberships persist for remaining users. |
| internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_removes_them_from_local_groups | Golden output ensuring local group file updates on user deletion. |
| internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_group_keeps_its_members_in_the_db | Golden output for manager-level group deletion where members remain. |
| internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_shared_group_leaves_other_groups_intact | Golden output for manager-level shared group deletion. |
| internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_users_for_which_the_group_is_primary | Golden output for primary-group lookup. |
| internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_multiple_users_when_group_is_primary_for_several | Golden output for primary-group lookup with multiple users. |
| internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_when_no_user_has_it_as_primary | Golden output for no matches. |
| internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_for_shared_group_that_is_not_primary | Golden output for non-primary shared group. |
| internal/services/user/user.go | Adds DeleteUser/DeleteGroup RPC handlers and maps GroupIsPrimaryError to gRPC. |
| internal/services/user/user_test.go | Adds service tests for delete user/group paths (uppercase handling, broker failures, perms). |
| internal/services/user/testdata/delete-user.db.yaml | Adds service DB fixture covering delete-user scenarios. |
| internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user | Golden output for service delete-user. |
| internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user_with_uppercase | Golden output verifying case normalization. |
| internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group | Golden output for service delete-group. |
| internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group_with_uppercase | Golden output verifying case normalization for groups. |
| internal/services/testdata/golden/TestRegisterGRPCServices | Updates golden list of registered RPC methods. |
| internal/proto/authd/authd.proto | Adds DeleteUserRequest/DeleteGroupRequest and service methods. |
| internal/proto/authd/authd.pb.go | Regenerates protobuf types for new messages. |
| internal/proto/authd/authd_grpc.pb.go | Regenerates gRPC stubs for new RPC methods. |
| internal/brokers/broker.go | Extends broker interface and adds Broker.DeleteUser. |
| internal/brokers/dbusbroker.go | Implements broker-side DeleteUser over D-Bus. |
| internal/brokers/localbroker.go | Adds stub DeleteUser to satisfy broker interface. |
| internal/brokers/manager.go | Exports BrokerFromID and updates internal callers. |
| internal/brokers/broker_test.go | Adds tests for broker DeleteUser. |
| internal/testutils/broker.go | Extends broker mock to support DeleteUser and forced errors. |
| cmd/authctl/user/user.go | Registers authctl user delete subcommand. |
| cmd/authctl/user/delete.go | Implements authctl user delete with confirmation + --remove. |
| cmd/authctl/user/delete_test.go | Integration-style tests for authctl user delete. |
| cmd/authctl/user/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml | Test DB fixture for authctl user delete (temp home dirs). |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_user_success | Golden output for user deletion. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_removes_home_dir | Golden output for --remove behavior. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_without_remove_flag_keeps_home_dir | Golden output for keeping home directory. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_succeeds_when_home_dir_does_not_exist | Golden output for missing home dir. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_user_does_not_exist | Golden error output for NotFound. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_authd_is_unavailable | Golden error output for unavailable daemon. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_y | Golden output for confirmation flow. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_yes | Golden output for confirmation flow. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_case_insensitively | Golden output for confirmation flow. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_n | Golden output for abort flow. |
| cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_empty_input | Golden output for abort flow. |
| cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args | Updates command list to include delete. |
| cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag | Updates help output to include delete. |
| cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag | Updates usage output to include delete. |
| cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command | Updates usage output to include delete. |
| cmd/authctl/group/group.go | Registers authctl group delete subcommand. |
| cmd/authctl/group/delete.go | Implements authctl group delete with confirmation. |
| cmd/authctl/group/delete_test.go | Integration-style tests for authctl group delete. |
| cmd/authctl/group/testdata/db/multiple_users_and_groups.db.yaml | Test DB fixture for authctl group delete scenarios. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Delete_group_success | Golden output for group deletion. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_does_not_exist | Golden error output for NotFound. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_authd_is_unavailable | Golden error output for unavailable daemon. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_is_primary_group_of_a_user | Golden error output for primary-group constraint. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_y | Golden output for confirmation flow. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_yes | Golden output for confirmation flow. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_case_insensitively | Golden output for confirmation flow. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_n | Golden output for abort flow. |
| cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_empty_input | Golden output for abort flow. |
| cmd/authctl/group/testdata/golden/TestGroupCommand/Usage_message_when_no_args | Updates command list to include delete. |
| cmd/authctl/group/testdata/golden/TestGroupCommand/Help_flag | Updates help output to include delete. |
| cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_flag | Updates usage output to include delete. |
| cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_command | Updates usage output to include delete. |
| debian/authd.service.in | Updates capability bounding set for delete-home support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| lockedEntries, unlockEntries, err := localentries.WithUserDBLock() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer func() { err = errors.Join(err, unlockEntries()) }() | ||
|
|
There was a problem hiding this comment.
The deferred errors.Join(err, unlockEntries()) is ineffective here because DeleteUser doesn’t use a named return value. Any error from unlockEntries() will be silently dropped, unlike other methods in this file that use a named err return. Consider switching DeleteUser to func (...) (err error) (or explicitly calling unlockEntries() and handling/joining its error before returning) to preserve unlock failures consistently.
| func usersWithPrimaryGroup(db queryable, gid uint32) ([]UserRow, error) { | ||
| query := fmt.Sprintf(`SELECT %s FROM users WHERE gid = ?`, publicUserColumns) | ||
| rows, err := db.Query(query, gid) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("query error: %w", err) | ||
| } | ||
| defer closeRows(rows) |
There was a problem hiding this comment.
UsersWithPrimaryGroup builds its result order from the database without an ORDER BY. That can make IsGroupPrimaryForUsers output (and GroupIsPrimaryError messages) nondeterministic across SQLite versions/plans. Adding an explicit order (e.g. ORDER BY name or ORDER BY uid) would make results stable and avoid flaky tests / inconsistent error strings.
| // Notify the broker so it can clean up any broker side data (tokens, cached | ||
| // passwords, etc.) stored for this user. We do this after the DB-side user deletion | ||
| // so that a broker side failure does not leave the user dangling in the DB. | ||
| // The local broker has no remote data, so skip it. | ||
| if brokerID != "" && brokerID != brokers.LocalBrokerName { | ||
| broker, err := s.brokerManager.BrokerFromID(brokerID) | ||
| if err != nil { | ||
| log.Errorf(ctx, "DeleteUser: could not find broker %q for user %q: %v", brokerID, name, err) | ||
| return nil, grpcError(err) | ||
| } | ||
| if err := broker.DeleteUser(ctx, name); err != nil { | ||
| log.Errorf(ctx, "DeleteUser: broker side cleanup for user %q failed: %v", name, err) | ||
| return nil, grpcError(err) | ||
| } |
There was a problem hiding this comment.
DeleteUser deletes the DB user before resolving the broker instance. If BrokerFromID (or the subsequent broker.DeleteUser) fails, this RPC returns an error even though the user has already been removed from the authd DB. To avoid partial-failure semantics, consider resolving the broker (and verifying it exists) before deleting the user, or treat broker-side cleanup failures as warnings and still return success after DB deletion.
| func (b Broker) DeleteUser(ctx context.Context, username string) error { | ||
| log.Debugf(context.TODO(), "Deleting user %q", username) | ||
| return b.brokerer.DeleteUser(ctx, username) |
There was a problem hiding this comment.
This log call uses context.TODO() instead of the provided ctx, which drops request-scoped context (cancellation, trace IDs, etc.) from logs. Use ctx here to keep logging consistent with the rest of the broker methods.


Closes #640
This adds subcommands to authctl to allow deleting a user/group using CLI.
This also contains relevant tests to verify the functionality.