diff --git a/cmd/authctl/group/delete.go b/cmd/authctl/group/delete.go new file mode 100644 index 0000000000..28d66b5832 --- /dev/null +++ b/cmd/authctl/group/delete.go @@ -0,0 +1,75 @@ +package group + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + "github.com/canonical/authd/cmd/authctl/internal/client" + "github.com/canonical/authd/cmd/authctl/internal/completion" + "github.com/canonical/authd/cmd/authctl/internal/log" + "github.com/canonical/authd/internal/proto/authd" + "github.com/spf13/cobra" +) + +const warningMessage = ` +WARNING: Deleting a group that still owns files on the filesystem can lead to +security issues. Any existing files owned by this group's GID may become +accessible to a different group that is later assigned the same GID. +` + +// deleteGroupCmd is a command to delete a group from the authd database. +var deleteGroupCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a group managed by authd", + Long: "Delete a group from the authd database.\n\n" + warningMessage + "\n\nThe command must be run as root.", + Example: ` # Delete group "staff" from the authd database + authctl group delete staff + + # Delete group "staff" without confirmation prompt + authctl group delete --yes staff`, + Args: cobra.ExactArgs(1), + ValidArgsFunction: completion.Groups, + RunE: runDeleteGroup, +} + +var deleteGroupYes bool + +func init() { + deleteGroupCmd.Flags().BoolVarP(&deleteGroupYes, "yes", "y", false, "Skip confirmation prompt") +} + +func runDeleteGroup(cmd *cobra.Command, args []string) error { + name := args[0] + + if !deleteGroupYes { + log.Warning(warningMessage) + fmt.Fprintf(os.Stderr, "Are you sure you want to delete group %q? [y/N] ", name) + + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read confirmation: %w", err) + } + answer = strings.TrimSpace(strings.ToLower(answer)) + if answer != "y" && answer != "yes" { + log.Info("Aborted.") + return nil + } + } + + c, err := client.NewUserServiceClient() + if err != nil { + return err + } + + _, err = c.DeleteGroup(context.Background(), &authd.DeleteGroupRequest{Name: name}) + if err != nil { + return err + } + + log.Infof("Group %q has been deleted from the authd database.", name) + return nil +} diff --git a/cmd/authctl/group/delete_test.go b/cmd/authctl/group/delete_test.go new file mode 100644 index 0000000000..83ea44e8fc --- /dev/null +++ b/cmd/authctl/group/delete_test.go @@ -0,0 +1,98 @@ +package group_test + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/canonical/authd/internal/testutils" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" +) + +func TestGroupDeleteCommand(t *testing.T) { + daemonSocket := testutils.StartAuthd(t, daemonPath, + testutils.WithGroupFile(filepath.Join("testdata", "empty.group")), + testutils.WithPreviousDBState("multiple_users_and_groups"), + testutils.WithCurrentUserAsRoot, + ) + + err := os.Setenv("AUTHD_SOCKET", daemonSocket) + require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable") + + tests := map[string]struct { + args []string + stdin string + authdUnavailable bool + + expectedExitCode int + }{ + "Delete_group_success": { + args: []string{"delete", "--yes", "group3-nonprimary"}, + expectedExitCode: 0, + }, + + "Confirmation_prompt_accepted_with_y": { + args: []string{"delete", "group4-nonprimary"}, + stdin: "y\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_accepted_with_yes": { + args: []string{"delete", "group5-nonprimary"}, + stdin: "yes\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_accepted_case_insensitively": { + args: []string{"delete", "group6-nonprimary"}, + stdin: "YES\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_aborted_with_n": { + args: []string{"delete", "group1"}, + stdin: "n\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_aborted_with_empty_input": { + args: []string{"delete", "group1"}, + stdin: "\n", + expectedExitCode: 0, + }, + + "Error_when_group_does_not_exist": { + args: []string{"delete", "--yes", "nonexistent"}, + expectedExitCode: int(codes.NotFound), + }, + "Error_when_authd_is_unavailable": { + args: []string{"delete", "--yes", "group1"}, + authdUnavailable: true, + expectedExitCode: int(codes.Unavailable), + }, + "Error_when_group_is_primary_group_of_a_user": { + args: []string{"delete", "--yes", "group1"}, + expectedExitCode: int(codes.FailedPrecondition), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if tc.authdUnavailable { + origValue := os.Getenv("AUTHD_SOCKET") + err := os.Setenv("AUTHD_SOCKET", "/non-existent") + require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable") + t.Cleanup(func() { + err := os.Setenv("AUTHD_SOCKET", origValue) + require.NoError(t, err, "Failed to restore AUTHD_SOCKET environment variable") + }) + } + + //nolint:gosec // G204 it's safe to use exec.Command with a variable here + cmd := exec.Command(authctlPath, append([]string{"group"}, tc.args...)...) + if tc.stdin != "" { + cmd.Stdin = strings.NewReader(tc.stdin) + } + testutils.CheckCommand(t, cmd, tc.expectedExitCode) + }) + } +} diff --git a/cmd/authctl/group/group.go b/cmd/authctl/group/group.go index 01a7c2b88b..e69752700a 100644 --- a/cmd/authctl/group/group.go +++ b/cmd/authctl/group/group.go @@ -15,4 +15,5 @@ var GroupCmd = &cobra.Command{ func init() { GroupCmd.AddCommand(setGIDCmd) + GroupCmd.AddCommand(deleteGroupCmd) } diff --git a/cmd/authctl/group/testdata/db/multiple_users_and_groups.db.yaml b/cmd/authctl/group/testdata/db/multiple_users_and_groups.db.yaml new file mode 100644 index 0000000000..243e4eec40 --- /dev/null +++ b/cmd/authctl/group/testdata/db/multiple_users_and_groups.db.yaml @@ -0,0 +1,43 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /tmp/authd-delete-cmd-test/home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /tmp/authd-delete-cmd-test/home/user2@example.com + shell: /bin/dash + broker_id: broker-id +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2 + gid: 22222 + ugid: "23456781" + - name: group3-nonprimary + gid: 33333 + ugid: "34567812" + - name: group4-nonprimary + gid: 44444 + ugid: "45678123" + - name: group5-nonprimary + gid: 55555 + ugid: "56781234" + - name: group6-nonprimary + gid: 66666 + ugid: "67812345" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 1111 + gid: 33333 diff --git a/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_command b/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_command index ecd4f9d8a8..0c7855b36a 100644 --- a/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_command +++ b/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_command @@ -4,6 +4,7 @@ Usage: Available Commands: set-gid Set the GID of a group managed by authd + delete Delete a group managed by authd Flags: -h, --help help for group diff --git a/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_flag b/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_flag index 7ca5da1f79..9f10847816 100644 --- a/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_flag +++ b/cmd/authctl/group/testdata/golden/TestGroupCommand/Error_on_invalid_flag @@ -4,6 +4,7 @@ Usage: Available Commands: set-gid Set the GID of a group managed by authd + delete Delete a group managed by authd Flags: -h, --help help for group diff --git a/cmd/authctl/group/testdata/golden/TestGroupCommand/Help_flag b/cmd/authctl/group/testdata/golden/TestGroupCommand/Help_flag index ad64bb0b01..920a9debac 100644 --- a/cmd/authctl/group/testdata/golden/TestGroupCommand/Help_flag +++ b/cmd/authctl/group/testdata/golden/TestGroupCommand/Help_flag @@ -6,6 +6,7 @@ Usage: Available Commands: set-gid Set the GID of a group managed by authd + delete Delete a group managed by authd Flags: -h, --help help for group diff --git a/cmd/authctl/group/testdata/golden/TestGroupCommand/Usage_message_when_no_args b/cmd/authctl/group/testdata/golden/TestGroupCommand/Usage_message_when_no_args index 1c53f30766..84e3fa483b 100644 --- a/cmd/authctl/group/testdata/golden/TestGroupCommand/Usage_message_when_no_args +++ b/cmd/authctl/group/testdata/golden/TestGroupCommand/Usage_message_when_no_args @@ -4,6 +4,7 @@ Usage: Available Commands: set-gid Set the GID of a group managed by authd + delete Delete a group managed by authd Flags: -h, --help help for group diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_empty_input b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_empty_input new file mode 100644 index 0000000000..e3ce99a4f2 --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_empty_input @@ -0,0 +1,6 @@ + +WARNING: Deleting a group that still owns files on the filesystem can lead to +security issues. Any existing files owned by this group's GID may become +accessible to a different group that is later assigned the same GID. + +Are you sure you want to delete group "group1"? [y/N] Aborted. diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_n b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_n new file mode 100644 index 0000000000..e3ce99a4f2 --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_aborted_with_n @@ -0,0 +1,6 @@ + +WARNING: Deleting a group that still owns files on the filesystem can lead to +security issues. Any existing files owned by this group's GID may become +accessible to a different group that is later assigned the same GID. + +Are you sure you want to delete group "group1"? [y/N] Aborted. diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_case_insensitively b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_case_insensitively new file mode 100644 index 0000000000..b45510c281 --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_case_insensitively @@ -0,0 +1,6 @@ + +WARNING: Deleting a group that still owns files on the filesystem can lead to +security issues. Any existing files owned by this group's GID may become +accessible to a different group that is later assigned the same GID. + +Are you sure you want to delete group "group6-nonprimary"? [y/N] Group "group6-nonprimary" has been deleted from the authd database. diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_y b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_y new file mode 100644 index 0000000000..a15ef30877 --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_y @@ -0,0 +1,6 @@ + +WARNING: Deleting a group that still owns files on the filesystem can lead to +security issues. Any existing files owned by this group's GID may become +accessible to a different group that is later assigned the same GID. + +Are you sure you want to delete group "group4-nonprimary"? [y/N] Group "group4-nonprimary" has been deleted from the authd database. diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_yes b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_yes new file mode 100644 index 0000000000..889a65d4b8 --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Confirmation_prompt_accepted_with_yes @@ -0,0 +1,6 @@ + +WARNING: Deleting a group that still owns files on the filesystem can lead to +security issues. Any existing files owned by this group's GID may become +accessible to a different group that is later assigned the same GID. + +Are you sure you want to delete group "group5-nonprimary"? [y/N] Group "group5-nonprimary" has been deleted from the authd database. diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Delete_group_success b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Delete_group_success new file mode 100644 index 0000000000..d93f04ed8e --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Delete_group_success @@ -0,0 +1 @@ +Group "group3-nonprimary" has been deleted from the authd database. diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_authd_is_unavailable b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_authd_is_unavailable new file mode 100644 index 0000000000..ba5b5abcba --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_authd_is_unavailable @@ -0,0 +1 @@ +Error: connection error: desc = "transport: Error while dialing: dial unix /non-existent: connect: no such file or directory" diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_does_not_exist b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_does_not_exist new file mode 100644 index 0000000000..420a3ade03 --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_does_not_exist @@ -0,0 +1 @@ +Error: group "nonexistent" not found diff --git a/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_is_primary_group_of_a_user b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_is_primary_group_of_a_user new file mode 100644 index 0000000000..b67a275f44 --- /dev/null +++ b/cmd/authctl/group/testdata/golden/TestGroupDeleteCommand/Error_when_group_is_primary_group_of_a_user @@ -0,0 +1 @@ +Error: group "group1" is the primary group of user(s): user1@example.com diff --git a/cmd/authctl/user/delete.go b/cmd/authctl/user/delete.go new file mode 100644 index 0000000000..539160acca --- /dev/null +++ b/cmd/authctl/user/delete.go @@ -0,0 +1,86 @@ +package user + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + "github.com/canonical/authd/cmd/authctl/internal/client" + "github.com/canonical/authd/cmd/authctl/internal/completion" + "github.com/canonical/authd/cmd/authctl/internal/log" + "github.com/canonical/authd/internal/proto/authd" + "github.com/spf13/cobra" +) + +const warningMessage = ` +WARNING: Deleting a user that still owns files on the filesystem can lead to +security issues. Any existing files owned by this user's UID may become +accessible to a different user that is later assigned the same UID. If the +user is later re-created, they may be assigned a new UID, breaking ownership +of their existing home directory and files. + +If you only want to prevent the user from logging in, consider using +'authctl user lock' instead. A locked user retains their UID, ensuring +no other user can be assigned the same UID. +` + +// deleteCmd is a command to delete a user from the authd database. +var deleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a user managed by authd", + Long: "Delete a user from the authd database.\n\n" + warningMessage + "\n\nThe command must be run as root.", + Example: ` # Delete user "alice" from the authd database + authctl user delete alice + + # Delete user "alice" without confirmation prompt + authctl user delete --yes alice + + # Delete user "alice" and remove their home directory + authctl user delete --remove alice`, + Args: cobra.ExactArgs(1), + ValidArgsFunction: completion.Users, + RunE: runDeleteUser, +} + +var deleteUserYes bool +var deleteUserRemoveHome bool + +func init() { + deleteCmd.Flags().BoolVarP(&deleteUserYes, "yes", "y", false, "Skip confirmation prompt") + deleteCmd.Flags().BoolVarP(&deleteUserRemoveHome, "remove", "r", false, "Remove the user's home directory") +} + +func runDeleteUser(cmd *cobra.Command, args []string) error { + name := args[0] + + if !deleteUserYes { + log.Warning(warningMessage) + fmt.Fprintf(os.Stderr, "Are you sure you want to delete user %q? [y/N] ", name) + + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read confirmation: %w", err) + } + answer = strings.TrimSpace(strings.ToLower(answer)) + if answer != "y" && answer != "yes" { + log.Info("Aborted.") + return nil + } + } + + c, err := client.NewUserServiceClient() + if err != nil { + return err + } + + _, err = c.DeleteUser(context.Background(), &authd.DeleteUserRequest{Name: name, RemoveHome: deleteUserRemoveHome}) + if err != nil { + return err + } + + log.Infof("User %q has been deleted from the authd database.", name) + return nil +} diff --git a/cmd/authctl/user/delete_test.go b/cmd/authctl/user/delete_test.go new file mode 100644 index 0000000000..bc1fbf2bc2 --- /dev/null +++ b/cmd/authctl/user/delete_test.go @@ -0,0 +1,133 @@ +package user_test + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/canonical/authd/internal/testutils" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" +) + +const homeBasePath = "/tmp/authd-delete-cmd-test/home" + +func TestUserDeleteCommand(t *testing.T) { + daemonSocket := testutils.StartAuthd(t, daemonPath, + testutils.WithGroupFile(filepath.Join("testdata", "empty.group")), + testutils.WithPreviousDBState("multiple_users_and_groups_with_tmp_home"), + testutils.WithCurrentUserAsRoot, + ) + t.Cleanup(func() { _ = os.RemoveAll(homeBasePath) }) + + err := os.Setenv("AUTHD_SOCKET", daemonSocket) + require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable") + + tests := map[string]struct { + args []string + stdin string + authdUnavailable bool + + createHomeDir bool + wantHomeDirRemoved bool + + expectedExitCode int + }{ + "Delete_user_success": { + args: []string{"delete", "--yes", "user1@example.com"}, + expectedExitCode: 0, + }, + + "Confirmation_prompt_accepted_with_y": { + args: []string{"delete", "user2@example.com"}, + stdin: "y\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_accepted_with_yes": { + args: []string{"delete", "user3@example.com"}, + stdin: "yes\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_accepted_case_insensitively": { + args: []string{"delete", "user4@example.com"}, + stdin: "YES\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_aborted_with_n": { + args: []string{"delete", "user1@example.com"}, + stdin: "n\n", + expectedExitCode: 0, + }, + "Confirmation_prompt_aborted_with_empty_input": { + args: []string{"delete", "user1@example.com"}, + stdin: "\n", + expectedExitCode: 0, + }, + + "Delete_with_remove_flag_removes_home_dir": { + args: []string{"delete", "--yes", "--remove", "user5@example.com"}, + createHomeDir: true, + wantHomeDirRemoved: true, + expectedExitCode: 0, + }, + "Delete_without_remove_flag_keeps_home_dir": { + args: []string{"delete", "--yes", "user6@example.com"}, + createHomeDir: true, + expectedExitCode: 0, + }, + "Delete_with_remove_flag_succeeds_when_home_dir_does_not_exist": { + args: []string{"delete", "--yes", "--remove", "user7@example.com"}, + wantHomeDirRemoved: true, + expectedExitCode: 0, + }, + + "Error_when_user_does_not_exist": { + args: []string{"delete", "--yes", "nonexistent@example.com"}, + expectedExitCode: int(codes.NotFound), + }, + "Error_when_authd_is_unavailable": { + args: []string{"delete", "--yes", "user1@example.com"}, + authdUnavailable: true, + expectedExitCode: int(codes.Unavailable), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if tc.authdUnavailable { + origValue := os.Getenv("AUTHD_SOCKET") + err := os.Setenv("AUTHD_SOCKET", "/non-existent") + require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable") + t.Cleanup(func() { + err := os.Setenv("AUTHD_SOCKET", origValue) + require.NoError(t, err, "Failed to restore AUTHD_SOCKET environment variable") + }) + } + + // Extract the username from the last element of args + username := tc.args[len(tc.args)-1] + homeDir := filepath.Join(homeBasePath, username) + + if tc.createHomeDir { + err := os.MkdirAll(homeDir, 0o700) + require.NoError(t, err, "Setup: failed to create home directory %q", homeDir) + t.Cleanup(func() { _ = os.RemoveAll(homeDir) }) + } + + //nolint:gosec // G204 it's safe to use exec.Command with a variable here + cmd := exec.Command(authctlPath, append([]string{"user"}, tc.args...)...) + if tc.stdin != "" { + cmd.Stdin = strings.NewReader(tc.stdin) + } + testutils.CheckCommand(t, cmd, tc.expectedExitCode) + + if tc.wantHomeDirRemoved { + require.NoDirExists(t, homeDir, "Home directory %q should have been removed", homeDir) + } else if tc.createHomeDir { + require.DirExists(t, homeDir, "Home directory %q should still exist", homeDir) + } + }) + } +} diff --git a/cmd/authctl/user/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml b/cmd/authctl/user/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml new file mode 100644 index 0000000000..4b23b0e0fd --- /dev/null +++ b/cmd/authctl/user/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml @@ -0,0 +1,89 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /tmp/authd-delete-cmd-test/home/user1@example.com + shell: /bin/bash + broker_id: "2221040704" + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /tmp/authd-delete-cmd-test/home/user2@example.com + shell: /bin/dash + broker_id: "2221040704" + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /tmp/authd-delete-cmd-test/home/user3@example.com + shell: /bin/zsh + broker_id: "2221040704" + - name: user4@example.com + uid: 4444 + gid: 44444 + gecos: User4 + dir: /tmp/authd-delete-cmd-test/home/user4@example.com + shell: /bin/sh + broker_id: "2221040704" + - name: user5@example.com + uid: 5555 + gid: 55555 + gecos: User5 + dir: /tmp/authd-delete-cmd-test/home/user5@example.com + shell: /bin/sh + broker_id: "2221040704" + - name: user6@example.com + uid: 6666 + gid: 66666 + gecos: User6 + dir: /tmp/authd-delete-cmd-test/home/user6@example.com + shell: /bin/sh + broker_id: "2221040704" + - name: user7@example.com + uid: 7777 + gid: 77777 + gecos: User7 + dir: /tmp/authd-delete-cmd-test/home/user7@example.com + shell: /bin/sh + broker_id: "2221040704" +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2 + gid: 22222 + ugid: "23456781" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: group5 + gid: 55555 + ugid: "56781234" + - name: group6 + gid: 66666 + ugid: "67812345" + - name: group7 + gid: 77777 + ugid: "78123456" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 3333 + gid: 33333 + - uid: 4444 + gid: 44444 + - uid: 5555 + gid: 55555 + - uid: 6666 + gid: 66666 + - uid: 7777 + gid: 77777 diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command index a66b03e2c9..e982445c1a 100644 --- a/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command @@ -6,6 +6,7 @@ Available Commands: lock Lock (disable) a user managed by authd unlock Unlock (enable) a user managed by authd set-uid Set the UID of a user managed by authd + delete Delete a user managed by authd Flags: -h, --help help for user diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag index 3408cec6d8..ac125a2dc3 100644 --- a/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag @@ -6,6 +6,7 @@ Available Commands: lock Lock (disable) a user managed by authd unlock Unlock (enable) a user managed by authd set-uid Set the UID of a user managed by authd + delete Delete a user managed by authd Flags: -h, --help help for user diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag b/cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag index ee4765c1cc..34105769a3 100644 --- a/cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag @@ -8,6 +8,7 @@ Available Commands: lock Lock (disable) a user managed by authd unlock Unlock (enable) a user managed by authd set-uid Set the UID of a user managed by authd + delete Delete a user managed by authd Flags: -h, --help help for user diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args b/cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args index d83ced5684..1162b86db4 100644 --- a/cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args @@ -6,6 +6,7 @@ Available Commands: lock Lock (disable) a user managed by authd unlock Unlock (enable) a user managed by authd set-uid Set the UID of a user managed by authd + delete Delete a user managed by authd Flags: -h, --help help for user diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_empty_input b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_empty_input new file mode 100644 index 0000000000..2f18a113ac --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_empty_input @@ -0,0 +1,12 @@ + +WARNING: Deleting a user that still owns files on the filesystem can lead to +security issues. Any existing files owned by this user's UID may become +accessible to a different user that is later assigned the same UID. If the +user is later re-created, they may be assigned a new UID, breaking ownership +of their existing home directory and files. + +If you only want to prevent the user from logging in, consider using +'authctl user lock' instead. A locked user retains their UID, ensuring +no other user can be assigned the same UID. + +Are you sure you want to delete user "user1@example.com"? [y/N] Aborted. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_n b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_n new file mode 100644 index 0000000000..2f18a113ac --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_aborted_with_n @@ -0,0 +1,12 @@ + +WARNING: Deleting a user that still owns files on the filesystem can lead to +security issues. Any existing files owned by this user's UID may become +accessible to a different user that is later assigned the same UID. If the +user is later re-created, they may be assigned a new UID, breaking ownership +of their existing home directory and files. + +If you only want to prevent the user from logging in, consider using +'authctl user lock' instead. A locked user retains their UID, ensuring +no other user can be assigned the same UID. + +Are you sure you want to delete user "user1@example.com"? [y/N] Aborted. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_case_insensitively b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_case_insensitively new file mode 100644 index 0000000000..98826a5fa5 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_case_insensitively @@ -0,0 +1,12 @@ + +WARNING: Deleting a user that still owns files on the filesystem can lead to +security issues. Any existing files owned by this user's UID may become +accessible to a different user that is later assigned the same UID. If the +user is later re-created, they may be assigned a new UID, breaking ownership +of their existing home directory and files. + +If you only want to prevent the user from logging in, consider using +'authctl user lock' instead. A locked user retains their UID, ensuring +no other user can be assigned the same UID. + +Are you sure you want to delete user "user4@example.com"? [y/N] User "user4@example.com" has been deleted from the authd database. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_y b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_y new file mode 100644 index 0000000000..a73a607054 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_y @@ -0,0 +1,12 @@ + +WARNING: Deleting a user that still owns files on the filesystem can lead to +security issues. Any existing files owned by this user's UID may become +accessible to a different user that is later assigned the same UID. If the +user is later re-created, they may be assigned a new UID, breaking ownership +of their existing home directory and files. + +If you only want to prevent the user from logging in, consider using +'authctl user lock' instead. A locked user retains their UID, ensuring +no other user can be assigned the same UID. + +Are you sure you want to delete user "user2@example.com"? [y/N] User "user2@example.com" has been deleted from the authd database. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_yes b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_yes new file mode 100644 index 0000000000..4bfe6c69a1 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Confirmation_prompt_accepted_with_yes @@ -0,0 +1,12 @@ + +WARNING: Deleting a user that still owns files on the filesystem can lead to +security issues. Any existing files owned by this user's UID may become +accessible to a different user that is later assigned the same UID. If the +user is later re-created, they may be assigned a new UID, breaking ownership +of their existing home directory and files. + +If you only want to prevent the user from logging in, consider using +'authctl user lock' instead. A locked user retains their UID, ensuring +no other user can be assigned the same UID. + +Are you sure you want to delete user "user3@example.com"? [y/N] User "user3@example.com" has been deleted from the authd database. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_user_success b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_user_success new file mode 100644 index 0000000000..40eea8e487 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_user_success @@ -0,0 +1 @@ +User "user1@example.com" has been deleted from the authd database. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_removes_home_dir b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_removes_home_dir new file mode 100644 index 0000000000..74d27210b2 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_removes_home_dir @@ -0,0 +1 @@ +User "user5@example.com" has been deleted from the authd database. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_succeeds_when_home_dir_does_not_exist b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_succeeds_when_home_dir_does_not_exist new file mode 100644 index 0000000000..29b258bbca --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_with_remove_flag_succeeds_when_home_dir_does_not_exist @@ -0,0 +1 @@ +User "user7@example.com" has been deleted from the authd database. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_without_remove_flag_keeps_home_dir b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_without_remove_flag_keeps_home_dir new file mode 100644 index 0000000000..0773a5cf07 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Delete_without_remove_flag_keeps_home_dir @@ -0,0 +1 @@ +User "user6@example.com" has been deleted from the authd database. diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_authd_is_unavailable b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_authd_is_unavailable new file mode 100644 index 0000000000..ba5b5abcba --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_authd_is_unavailable @@ -0,0 +1 @@ +Error: connection error: desc = "transport: Error while dialing: dial unix /non-existent: connect: no such file or directory" diff --git a/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_user_does_not_exist b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_user_does_not_exist new file mode 100644 index 0000000000..435ee7d2be --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserDeleteCommand/Error_when_user_does_not_exist @@ -0,0 +1 @@ +Error: user "nonexistent@example.com" not found diff --git a/cmd/authctl/user/user.go b/cmd/authctl/user/user.go index 109743aa17..4f6450459a 100644 --- a/cmd/authctl/user/user.go +++ b/cmd/authctl/user/user.go @@ -17,4 +17,5 @@ func init() { UserCmd.AddCommand(lockCmd) UserCmd.AddCommand(unlockCmd) UserCmd.AddCommand(setUIDCmd) + UserCmd.AddCommand(deleteCmd) } diff --git a/debian/authd.service.in b/debian/authd.service.in index b3729ad741..f4a635bda5 100644 --- a/debian/authd.service.in +++ b/debian/authd.service.in @@ -87,10 +87,11 @@ SystemCallFilter=@system-service ProcSubset=pid # CAP_CHOWN: Required by gpasswd to alter the shadow files. -# CAP_DAC_READ_SEARCH: Required by the chown system call to change ownership of -# files not owned by the user. We need this to change the -# ownership of the user's home directory when changing the -# user's UID. +# CAP_DAC_OVERRIDE: Required to bypass discretionary access control checks. +# Needed when chowning files not owned by the process (e.g. +# changing ownership of the user's home directory when +# updating a UID) and when removing the home directory during +# user deletion. # CAP_SYS_PTRACE: Required by CheckUserBusy used by SetUserID to check if any # running processes are owned by the UID being modified. -CapabilityBoundingSet=CAP_CHOWN CAP_DAC_READ_SEARCH CAP_SYS_PTRACE +CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_SYS_PTRACE diff --git a/internal/brokers/broker.go b/internal/brokers/broker.go index eddc7da7ff..8e642a5a63 100644 --- a/internal/brokers/broker.go +++ b/internal/brokers/broker.go @@ -31,6 +31,7 @@ type brokerer interface { CancelIsAuthenticated(ctx context.Context, sessionID string) UserPreCheck(ctx context.Context, username string) (userinfo string, err error) + DeleteUser(ctx context.Context, username string) error } // Broker represents a broker object that can be used for authentication. @@ -247,6 +248,12 @@ func (b Broker) UserPreCheck(ctx context.Context, username string) (userinfo str return b.brokerer.UserPreCheck(ctx, username) } +// DeleteUser calls the broker to delete any broker side data associated with the user. +func (b Broker) DeleteUser(ctx context.Context, username string) error { + log.Debugf(context.TODO(), "Deleting user %q", username) + return b.brokerer.DeleteUser(ctx, username) +} + // generateValidators generates layout validators based on what is supported by the system. // // The layout validators are in the form: diff --git a/internal/brokers/broker_test.go b/internal/brokers/broker_test.go index 7abd54ff03..e547d9f1f7 100644 --- a/internal/brokers/broker_test.go +++ b/internal/brokers/broker_test.go @@ -346,6 +346,33 @@ func TestUserPreCheck(t *testing.T) { } } +func TestDeleteUser(t *testing.T) { + t.Parallel() + + b := newBrokerForTests(t, "", "") + + tests := map[string]struct { + username string + + wantErr bool + }{ + "Successfully_delete_user": {username: "user1@example.com"}, + "Error_when_broker_returns_error": {username: "delete_error@example.com", wantErr: true}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := b.DeleteUser(context.Background(), tc.username) + if tc.wantErr { + require.Error(t, err, "DeleteUser should return an error, but did not") + return + } + require.NoError(t, err, "DeleteUser should not return an error, but did") + }) + } +} + func newBrokerForTests(t *testing.T, cfgDir, brokerCfg string) (b brokers.Broker) { t.Helper() diff --git a/internal/brokers/dbusbroker.go b/internal/brokers/dbusbroker.go index 759ad247fa..c3e87cefa3 100644 --- a/internal/brokers/dbusbroker.go +++ b/internal/brokers/dbusbroker.go @@ -140,6 +140,14 @@ func (b dbusBroker) UserPreCheck(ctx context.Context, username string) (userinfo return userinfo, nil } +// DeleteUser calls the corresponding method on the broker bus to delete broker side user data. +func (b dbusBroker) DeleteUser(ctx context.Context, username string) error { + if _, err := b.call(ctx, "DeleteUser", username); err != nil { + return err + } + return nil +} + // call is an abstraction over dbus calls to ensure we wrap the returned error to an ErrorToDisplay. // All wrapped errors will be logged, but not returned to the UI. func (b dbusBroker) call(ctx context.Context, method string, args ...interface{}) (*dbus.Call, error) { diff --git a/internal/brokers/localbroker.go b/internal/brokers/localbroker.go index c0730fced7..b8414e75e0 100644 --- a/internal/brokers/localbroker.go +++ b/internal/brokers/localbroker.go @@ -42,3 +42,8 @@ func (b localBroker) CancelIsAuthenticated(ctx context.Context, sessionID string func (b localBroker) UserPreCheck(ctx context.Context, username string) (string, error) { return "", errors.New("UserPreCheck should never be called on local broker") } + +//nolint:unused // We still need localBroker to implement the brokerer interface, even though this method should never be called on it. +func (b localBroker) DeleteUser(ctx context.Context, username string) error { + return errors.New("DeleteUser should never be called on local broker") +} diff --git a/internal/brokers/manager.go b/internal/brokers/manager.go index 43e9be1295..d255498bb2 100644 --- a/internal/brokers/manager.go +++ b/internal/brokers/manager.go @@ -127,7 +127,7 @@ func (m *Manager) AvailableBrokers() (r []*Broker) { // SetDefaultBrokerForUser memorizes which broker was used for which user. func (m *Manager) SetDefaultBrokerForUser(brokerID, username string) error { - broker, err := m.brokerFromID(brokerID) + broker, err := m.BrokerFromID(brokerID) if err != nil { return fmt.Errorf("invalid broker: %v", err) } @@ -152,7 +152,7 @@ func (m *Manager) BrokerFromSessionID(id string) (broker *Broker, err error) { // no session ID means local broker if id == "" { - return m.brokerFromID(LocalBrokerName) + return m.BrokerFromID(LocalBrokerName) } broker, exists := m.transactionsToBroker[id] @@ -165,7 +165,7 @@ func (m *Manager) BrokerFromSessionID(id string) (broker *Broker, err error) { // NewSession create a new session for the broker and store the sessionID on the manager. func (m *Manager) NewSession(brokerID, username, lang, mode string) (sessionID string, encryptionKey string, err error) { - broker, err := m.brokerFromID(brokerID) + broker, err := m.BrokerFromID(brokerID) if err != nil { return "", "", fmt.Errorf("invalid broker: %v", err) } @@ -209,8 +209,8 @@ func (m *Manager) BrokerExists(brokerID string) bool { return exists } -// brokerFromID returns the broker matching this brokerID. -func (m *Manager) brokerFromID(id string) (broker *Broker, err error) { +// BrokerFromID returns the broker matching this brokerID. +func (m *Manager) BrokerFromID(id string) (broker *Broker, err error) { broker, exists := m.brokers[id] if !exists { return nil, fmt.Errorf("no broker found matching %q", id) diff --git a/internal/proto/authd/authd.pb.go b/internal/proto/authd/authd.pb.go index 556a17b4ac..0fbe2a699b 100644 --- a/internal/proto/authd/authd.pb.go +++ b/internal/proto/authd/authd.pb.go @@ -1081,6 +1081,103 @@ func (x *UnlockUserRequest) GetName() string { return "" } +type DeleteUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // If true, remove the user's home directory. + RemoveHome bool `protobuf:"varint,2,opt,name=remove_home,json=removeHome,proto3" json:"remove_home,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteUserRequest) Reset() { + *x = DeleteUserRequest{} + mi := &file_authd_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserRequest) ProtoMessage() {} + +func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_authd_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead. +func (*DeleteUserRequest) Descriptor() ([]byte, []int) { + return file_authd_proto_rawDescGZIP(), []int{20} +} + +func (x *DeleteUserRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteUserRequest) GetRemoveHome() bool { + if x != nil { + return x.RemoveHome + } + return false +} + +type DeleteGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteGroupRequest) Reset() { + *x = DeleteGroupRequest{} + mi := &file_authd_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteGroupRequest) ProtoMessage() {} + +func (x *DeleteGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_authd_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteGroupRequest.ProtoReflect.Descriptor instead. +func (*DeleteGroupRequest) Descriptor() ([]byte, []int) { + return file_authd_proto_rawDescGZIP(), []int{21} +} + +func (x *DeleteGroupRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type GetGroupByNameRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -1090,7 +1187,7 @@ type GetGroupByNameRequest struct { func (x *GetGroupByNameRequest) Reset() { *x = GetGroupByNameRequest{} - mi := &file_authd_proto_msgTypes[20] + mi := &file_authd_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1102,7 +1199,7 @@ func (x *GetGroupByNameRequest) String() string { func (*GetGroupByNameRequest) ProtoMessage() {} func (x *GetGroupByNameRequest) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[20] + mi := &file_authd_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1115,7 +1212,7 @@ func (x *GetGroupByNameRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGroupByNameRequest.ProtoReflect.Descriptor instead. func (*GetGroupByNameRequest) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{20} + return file_authd_proto_rawDescGZIP(), []int{22} } func (x *GetGroupByNameRequest) GetName() string { @@ -1134,7 +1231,7 @@ type GetGroupByIDRequest struct { func (x *GetGroupByIDRequest) Reset() { *x = GetGroupByIDRequest{} - mi := &file_authd_proto_msgTypes[21] + mi := &file_authd_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1146,7 +1243,7 @@ func (x *GetGroupByIDRequest) String() string { func (*GetGroupByIDRequest) ProtoMessage() {} func (x *GetGroupByIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[21] + mi := &file_authd_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1159,7 +1256,7 @@ func (x *GetGroupByIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGroupByIDRequest.ProtoReflect.Descriptor instead. func (*GetGroupByIDRequest) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{21} + return file_authd_proto_rawDescGZIP(), []int{23} } func (x *GetGroupByIDRequest) GetId() uint32 { @@ -1182,7 +1279,7 @@ type SetUserIDRequest struct { func (x *SetUserIDRequest) Reset() { *x = SetUserIDRequest{} - mi := &file_authd_proto_msgTypes[22] + mi := &file_authd_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1194,7 +1291,7 @@ func (x *SetUserIDRequest) String() string { func (*SetUserIDRequest) ProtoMessage() {} func (x *SetUserIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[22] + mi := &file_authd_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1207,7 +1304,7 @@ func (x *SetUserIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetUserIDRequest.ProtoReflect.Descriptor instead. func (*SetUserIDRequest) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{22} + return file_authd_proto_rawDescGZIP(), []int{24} } func (x *SetUserIDRequest) GetName() string { @@ -1242,7 +1339,7 @@ type SetUserIDResponse struct { func (x *SetUserIDResponse) Reset() { *x = SetUserIDResponse{} - mi := &file_authd_proto_msgTypes[23] + mi := &file_authd_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1254,7 +1351,7 @@ func (x *SetUserIDResponse) String() string { func (*SetUserIDResponse) ProtoMessage() {} func (x *SetUserIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[23] + mi := &file_authd_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1267,7 +1364,7 @@ func (x *SetUserIDResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetUserIDResponse.ProtoReflect.Descriptor instead. func (*SetUserIDResponse) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{23} + return file_authd_proto_rawDescGZIP(), []int{25} } func (x *SetUserIDResponse) GetIdChanged() bool { @@ -1304,7 +1401,7 @@ type SetGroupIDRequest struct { func (x *SetGroupIDRequest) Reset() { *x = SetGroupIDRequest{} - mi := &file_authd_proto_msgTypes[24] + mi := &file_authd_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1316,7 +1413,7 @@ func (x *SetGroupIDRequest) String() string { func (*SetGroupIDRequest) ProtoMessage() {} func (x *SetGroupIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[24] + mi := &file_authd_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1329,7 +1426,7 @@ func (x *SetGroupIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGroupIDRequest.ProtoReflect.Descriptor instead. func (*SetGroupIDRequest) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{24} + return file_authd_proto_rawDescGZIP(), []int{26} } func (x *SetGroupIDRequest) GetName() string { @@ -1364,7 +1461,7 @@ type SetGroupIDResponse struct { func (x *SetGroupIDResponse) Reset() { *x = SetGroupIDResponse{} - mi := &file_authd_proto_msgTypes[25] + mi := &file_authd_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1376,7 +1473,7 @@ func (x *SetGroupIDResponse) String() string { func (*SetGroupIDResponse) ProtoMessage() {} func (x *SetGroupIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[25] + mi := &file_authd_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1389,7 +1486,7 @@ func (x *SetGroupIDResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGroupIDResponse.ProtoReflect.Descriptor instead. func (*SetGroupIDResponse) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{25} + return file_authd_proto_rawDescGZIP(), []int{27} } func (x *SetGroupIDResponse) GetIdChanged() bool { @@ -1427,7 +1524,7 @@ type User struct { func (x *User) Reset() { *x = User{} - mi := &file_authd_proto_msgTypes[26] + mi := &file_authd_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1439,7 +1536,7 @@ func (x *User) String() string { func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[26] + mi := &file_authd_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1452,7 +1549,7 @@ func (x *User) ProtoReflect() protoreflect.Message { // Deprecated: Use User.ProtoReflect.Descriptor instead. func (*User) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{26} + return file_authd_proto_rawDescGZIP(), []int{28} } func (x *User) GetName() string { @@ -1506,7 +1603,7 @@ type Users struct { func (x *Users) Reset() { *x = Users{} - mi := &file_authd_proto_msgTypes[27] + mi := &file_authd_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1518,7 +1615,7 @@ func (x *Users) String() string { func (*Users) ProtoMessage() {} func (x *Users) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[27] + mi := &file_authd_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1531,7 +1628,7 @@ func (x *Users) ProtoReflect() protoreflect.Message { // Deprecated: Use Users.ProtoReflect.Descriptor instead. func (*Users) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{27} + return file_authd_proto_rawDescGZIP(), []int{29} } func (x *Users) GetUsers() []*User { @@ -1554,7 +1651,7 @@ type Group struct { func (x *Group) Reset() { *x = Group{} - mi := &file_authd_proto_msgTypes[28] + mi := &file_authd_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1566,7 +1663,7 @@ func (x *Group) String() string { func (*Group) ProtoMessage() {} func (x *Group) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[28] + mi := &file_authd_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1579,7 +1676,7 @@ func (x *Group) ProtoReflect() protoreflect.Message { // Deprecated: Use Group.ProtoReflect.Descriptor instead. func (*Group) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{28} + return file_authd_proto_rawDescGZIP(), []int{30} } func (x *Group) GetName() string { @@ -1619,7 +1716,7 @@ type Groups struct { func (x *Groups) Reset() { *x = Groups{} - mi := &file_authd_proto_msgTypes[29] + mi := &file_authd_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1631,7 +1728,7 @@ func (x *Groups) String() string { func (*Groups) ProtoMessage() {} func (x *Groups) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[29] + mi := &file_authd_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1644,7 +1741,7 @@ func (x *Groups) ProtoReflect() protoreflect.Message { // Deprecated: Use Groups.ProtoReflect.Descriptor instead. func (*Groups) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{29} + return file_authd_proto_rawDescGZIP(), []int{31} } func (x *Groups) GetGroups() []*Group { @@ -1665,7 +1762,7 @@ type ABResponse_BrokerInfo struct { func (x *ABResponse_BrokerInfo) Reset() { *x = ABResponse_BrokerInfo{} - mi := &file_authd_proto_msgTypes[30] + mi := &file_authd_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1677,7 +1774,7 @@ func (x *ABResponse_BrokerInfo) String() string { func (*ABResponse_BrokerInfo) ProtoMessage() {} func (x *ABResponse_BrokerInfo) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[30] + mi := &file_authd_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1724,7 +1821,7 @@ type GAMResponse_AuthenticationMode struct { func (x *GAMResponse_AuthenticationMode) Reset() { *x = GAMResponse_AuthenticationMode{} - mi := &file_authd_proto_msgTypes[31] + mi := &file_authd_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1736,7 +1833,7 @@ func (x *GAMResponse_AuthenticationMode) String() string { func (*GAMResponse_AuthenticationMode) ProtoMessage() {} func (x *GAMResponse_AuthenticationMode) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[31] + mi := &file_authd_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1781,7 +1878,7 @@ type IARequest_AuthenticationData struct { func (x *IARequest_AuthenticationData) Reset() { *x = IARequest_AuthenticationData{} - mi := &file_authd_proto_msgTypes[32] + mi := &file_authd_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1793,7 +1890,7 @@ func (x *IARequest_AuthenticationData) String() string { func (*IARequest_AuthenticationData) ProtoMessage() {} func (x *IARequest_AuthenticationData) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[32] + mi := &file_authd_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1976,6 +2073,12 @@ const file_authd_proto_rawDesc = "" + "\x0fLockUserRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"'\n" + "\x11UnlockUserRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"H\n" + + "\x11DeleteUserRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n" + + "\vremove_home\x18\x02 \x01(\bR\n" + + "removeHome\"(\n" + + "\x12DeleteGroupRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"+\n" + "\x15GetGroupByNameRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"%\n" + @@ -2028,7 +2131,7 @@ const file_authd_proto_rawDesc = "" + "\x0fIsAuthenticated\x12\x10.authd.IARequest\x1a\x11.authd.IAResponse\x12,\n" + "\n" + "EndSession\x12\x10.authd.ESRequest\x1a\f.authd.Empty\x12<\n" + - "\x17SetDefaultBrokerForUser\x12\x13.authd.SDBFURequest\x1a\f.authd.Empty2\xb6\x04\n" + + "\x17SetDefaultBrokerForUser\x12\x13.authd.SDBFURequest\x1a\f.authd.Empty2\xa4\x05\n" + "\vUserService\x129\n" + "\rGetUserByName\x12\x1b.authd.GetUserByNameRequest\x1a\v.authd.User\x125\n" + "\vGetUserByID\x12\x19.authd.GetUserByIDRequest\x1a\v.authd.User\x12'\n" + @@ -2038,7 +2141,10 @@ const file_authd_proto_rawDesc = "" + "UnlockUser\x12\x18.authd.UnlockUserRequest\x1a\f.authd.Empty\x12>\n" + "\tSetUserID\x12\x17.authd.SetUserIDRequest\x1a\x18.authd.SetUserIDResponse\x12A\n" + "\n" + - "SetGroupID\x12\x18.authd.SetGroupIDRequest\x1a\x19.authd.SetGroupIDResponse\x12<\n" + + "SetGroupID\x12\x18.authd.SetGroupIDRequest\x1a\x19.authd.SetGroupIDResponse\x124\n" + + "\n" + + "DeleteUser\x12\x18.authd.DeleteUserRequest\x1a\f.authd.Empty\x126\n" + + "\vDeleteGroup\x12\x19.authd.DeleteGroupRequest\x1a\f.authd.Empty\x12<\n" + "\x0eGetGroupByName\x12\x1c.authd.GetGroupByNameRequest\x1a\f.authd.Group\x128\n" + "\fGetGroupByID\x12\x1a.authd.GetGroupByIDRequest\x1a\f.authd.Group\x12)\n" + "\n" + @@ -2057,7 +2163,7 @@ func file_authd_proto_rawDescGZIP() []byte { } var file_authd_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_authd_proto_msgTypes = make([]protoimpl.MessageInfo, 33) +var file_authd_proto_msgTypes = make([]protoimpl.MessageInfo, 35) var file_authd_proto_goTypes = []any{ (SessionMode)(0), // 0: authd.SessionMode (*Empty)(nil), // 1: authd.Empty @@ -2080,29 +2186,31 @@ var file_authd_proto_goTypes = []any{ (*GetUserByIDRequest)(nil), // 18: authd.GetUserByIDRequest (*LockUserRequest)(nil), // 19: authd.LockUserRequest (*UnlockUserRequest)(nil), // 20: authd.UnlockUserRequest - (*GetGroupByNameRequest)(nil), // 21: authd.GetGroupByNameRequest - (*GetGroupByIDRequest)(nil), // 22: authd.GetGroupByIDRequest - (*SetUserIDRequest)(nil), // 23: authd.SetUserIDRequest - (*SetUserIDResponse)(nil), // 24: authd.SetUserIDResponse - (*SetGroupIDRequest)(nil), // 25: authd.SetGroupIDRequest - (*SetGroupIDResponse)(nil), // 26: authd.SetGroupIDResponse - (*User)(nil), // 27: authd.User - (*Users)(nil), // 28: authd.Users - (*Group)(nil), // 29: authd.Group - (*Groups)(nil), // 30: authd.Groups - (*ABResponse_BrokerInfo)(nil), // 31: authd.ABResponse.BrokerInfo - (*GAMResponse_AuthenticationMode)(nil), // 32: authd.GAMResponse.AuthenticationMode - (*IARequest_AuthenticationData)(nil), // 33: authd.IARequest.AuthenticationData + (*DeleteUserRequest)(nil), // 21: authd.DeleteUserRequest + (*DeleteGroupRequest)(nil), // 22: authd.DeleteGroupRequest + (*GetGroupByNameRequest)(nil), // 23: authd.GetGroupByNameRequest + (*GetGroupByIDRequest)(nil), // 24: authd.GetGroupByIDRequest + (*SetUserIDRequest)(nil), // 25: authd.SetUserIDRequest + (*SetUserIDResponse)(nil), // 26: authd.SetUserIDResponse + (*SetGroupIDRequest)(nil), // 27: authd.SetGroupIDRequest + (*SetGroupIDResponse)(nil), // 28: authd.SetGroupIDResponse + (*User)(nil), // 29: authd.User + (*Users)(nil), // 30: authd.Users + (*Group)(nil), // 31: authd.Group + (*Groups)(nil), // 32: authd.Groups + (*ABResponse_BrokerInfo)(nil), // 33: authd.ABResponse.BrokerInfo + (*GAMResponse_AuthenticationMode)(nil), // 34: authd.GAMResponse.AuthenticationMode + (*IARequest_AuthenticationData)(nil), // 35: authd.IARequest.AuthenticationData } var file_authd_proto_depIdxs = []int32{ - 31, // 0: authd.ABResponse.brokers_infos:type_name -> authd.ABResponse.BrokerInfo + 33, // 0: authd.ABResponse.brokers_infos:type_name -> authd.ABResponse.BrokerInfo 0, // 1: authd.SBRequest.mode:type_name -> authd.SessionMode 9, // 2: authd.GAMRequest.supported_ui_layouts:type_name -> authd.UILayout - 32, // 3: authd.GAMResponse.authentication_modes:type_name -> authd.GAMResponse.AuthenticationMode + 34, // 3: authd.GAMResponse.authentication_modes:type_name -> authd.GAMResponse.AuthenticationMode 9, // 4: authd.SAMResponse.ui_layout_info:type_name -> authd.UILayout - 33, // 5: authd.IARequest.authentication_data:type_name -> authd.IARequest.AuthenticationData - 27, // 6: authd.Users.users:type_name -> authd.User - 29, // 7: authd.Groups.groups:type_name -> authd.Group + 35, // 5: authd.IARequest.authentication_data:type_name -> authd.IARequest.AuthenticationData + 29, // 6: authd.Users.users:type_name -> authd.User + 31, // 7: authd.Groups.groups:type_name -> authd.Group 1, // 8: authd.PAM.AvailableBrokers:input_type -> authd.Empty 2, // 9: authd.PAM.GetPreviousBroker:input_type -> authd.GPBRequest 6, // 10: authd.PAM.SelectBroker:input_type -> authd.SBRequest @@ -2116,31 +2224,35 @@ var file_authd_proto_depIdxs = []int32{ 1, // 18: authd.UserService.ListUsers:input_type -> authd.Empty 19, // 19: authd.UserService.LockUser:input_type -> authd.LockUserRequest 20, // 20: authd.UserService.UnlockUser:input_type -> authd.UnlockUserRequest - 23, // 21: authd.UserService.SetUserID:input_type -> authd.SetUserIDRequest - 25, // 22: authd.UserService.SetGroupID:input_type -> authd.SetGroupIDRequest - 21, // 23: authd.UserService.GetGroupByName:input_type -> authd.GetGroupByNameRequest - 22, // 24: authd.UserService.GetGroupByID:input_type -> authd.GetGroupByIDRequest - 1, // 25: authd.UserService.ListGroups:input_type -> authd.Empty - 4, // 26: authd.PAM.AvailableBrokers:output_type -> authd.ABResponse - 3, // 27: authd.PAM.GetPreviousBroker:output_type -> authd.GPBResponse - 7, // 28: authd.PAM.SelectBroker:output_type -> authd.SBResponse - 10, // 29: authd.PAM.GetAuthenticationModes:output_type -> authd.GAMResponse - 12, // 30: authd.PAM.SelectAuthenticationMode:output_type -> authd.SAMResponse - 14, // 31: authd.PAM.IsAuthenticated:output_type -> authd.IAResponse - 1, // 32: authd.PAM.EndSession:output_type -> authd.Empty - 1, // 33: authd.PAM.SetDefaultBrokerForUser:output_type -> authd.Empty - 27, // 34: authd.UserService.GetUserByName:output_type -> authd.User - 27, // 35: authd.UserService.GetUserByID:output_type -> authd.User - 28, // 36: authd.UserService.ListUsers:output_type -> authd.Users - 1, // 37: authd.UserService.LockUser:output_type -> authd.Empty - 1, // 38: authd.UserService.UnlockUser:output_type -> authd.Empty - 24, // 39: authd.UserService.SetUserID:output_type -> authd.SetUserIDResponse - 26, // 40: authd.UserService.SetGroupID:output_type -> authd.SetGroupIDResponse - 29, // 41: authd.UserService.GetGroupByName:output_type -> authd.Group - 29, // 42: authd.UserService.GetGroupByID:output_type -> authd.Group - 30, // 43: authd.UserService.ListGroups:output_type -> authd.Groups - 26, // [26:44] is the sub-list for method output_type - 8, // [8:26] is the sub-list for method input_type + 25, // 21: authd.UserService.SetUserID:input_type -> authd.SetUserIDRequest + 27, // 22: authd.UserService.SetGroupID:input_type -> authd.SetGroupIDRequest + 21, // 23: authd.UserService.DeleteUser:input_type -> authd.DeleteUserRequest + 22, // 24: authd.UserService.DeleteGroup:input_type -> authd.DeleteGroupRequest + 23, // 25: authd.UserService.GetGroupByName:input_type -> authd.GetGroupByNameRequest + 24, // 26: authd.UserService.GetGroupByID:input_type -> authd.GetGroupByIDRequest + 1, // 27: authd.UserService.ListGroups:input_type -> authd.Empty + 4, // 28: authd.PAM.AvailableBrokers:output_type -> authd.ABResponse + 3, // 29: authd.PAM.GetPreviousBroker:output_type -> authd.GPBResponse + 7, // 30: authd.PAM.SelectBroker:output_type -> authd.SBResponse + 10, // 31: authd.PAM.GetAuthenticationModes:output_type -> authd.GAMResponse + 12, // 32: authd.PAM.SelectAuthenticationMode:output_type -> authd.SAMResponse + 14, // 33: authd.PAM.IsAuthenticated:output_type -> authd.IAResponse + 1, // 34: authd.PAM.EndSession:output_type -> authd.Empty + 1, // 35: authd.PAM.SetDefaultBrokerForUser:output_type -> authd.Empty + 29, // 36: authd.UserService.GetUserByName:output_type -> authd.User + 29, // 37: authd.UserService.GetUserByID:output_type -> authd.User + 30, // 38: authd.UserService.ListUsers:output_type -> authd.Users + 1, // 39: authd.UserService.LockUser:output_type -> authd.Empty + 1, // 40: authd.UserService.UnlockUser:output_type -> authd.Empty + 26, // 41: authd.UserService.SetUserID:output_type -> authd.SetUserIDResponse + 28, // 42: authd.UserService.SetGroupID:output_type -> authd.SetGroupIDResponse + 1, // 43: authd.UserService.DeleteUser:output_type -> authd.Empty + 1, // 44: authd.UserService.DeleteGroup:output_type -> authd.Empty + 31, // 45: authd.UserService.GetGroupByName:output_type -> authd.Group + 31, // 46: authd.UserService.GetGroupByID:output_type -> authd.Group + 32, // 47: authd.UserService.ListGroups:output_type -> authd.Groups + 28, // [28:48] is the sub-list for method output_type + 8, // [8:28] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name 8, // [8:8] is the sub-list for extension extendee 0, // [0:8] is the sub-list for field type_name @@ -2152,8 +2264,8 @@ func file_authd_proto_init() { return } file_authd_proto_msgTypes[8].OneofWrappers = []any{} - file_authd_proto_msgTypes[30].OneofWrappers = []any{} - file_authd_proto_msgTypes[32].OneofWrappers = []any{ + file_authd_proto_msgTypes[32].OneofWrappers = []any{} + file_authd_proto_msgTypes[34].OneofWrappers = []any{ (*IARequest_AuthenticationData_Secret)(nil), (*IARequest_AuthenticationData_Wait)(nil), (*IARequest_AuthenticationData_Skip)(nil), @@ -2165,7 +2277,7 @@ func file_authd_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_authd_proto_rawDesc), len(file_authd_proto_rawDesc)), NumEnums: 1, - NumMessages: 33, + NumMessages: 35, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/proto/authd/authd.proto b/internal/proto/authd/authd.proto index df108d0f51..57693840b1 100644 --- a/internal/proto/authd/authd.proto +++ b/internal/proto/authd/authd.proto @@ -137,6 +137,8 @@ service UserService { rpc UnlockUser(UnlockUserRequest) returns (Empty); rpc SetUserID(SetUserIDRequest) returns (SetUserIDResponse); rpc SetGroupID(SetGroupIDRequest) returns (SetGroupIDResponse); + rpc DeleteUser(DeleteUserRequest) returns (Empty); + rpc DeleteGroup(DeleteGroupRequest) returns (Empty); rpc GetGroupByName(GetGroupByNameRequest) returns (Group); rpc GetGroupByID(GetGroupByIDRequest) returns (Group); @@ -160,6 +162,16 @@ message UnlockUserRequest{ string name = 1; } +message DeleteUserRequest{ + string name = 1; + // If true, remove the user's home directory. + bool remove_home = 2; +} + +message DeleteGroupRequest{ + string name = 1; +} + message GetGroupByNameRequest{ string name = 1; } diff --git a/internal/proto/authd/authd_grpc.pb.go b/internal/proto/authd/authd_grpc.pb.go index 8685f6fdca..41e0a8df2b 100644 --- a/internal/proto/authd/authd_grpc.pb.go +++ b/internal/proto/authd/authd_grpc.pb.go @@ -394,6 +394,8 @@ const ( UserService_UnlockUser_FullMethodName = "/authd.UserService/UnlockUser" UserService_SetUserID_FullMethodName = "/authd.UserService/SetUserID" UserService_SetGroupID_FullMethodName = "/authd.UserService/SetGroupID" + UserService_DeleteUser_FullMethodName = "/authd.UserService/DeleteUser" + UserService_DeleteGroup_FullMethodName = "/authd.UserService/DeleteGroup" UserService_GetGroupByName_FullMethodName = "/authd.UserService/GetGroupByName" UserService_GetGroupByID_FullMethodName = "/authd.UserService/GetGroupByID" UserService_ListGroups_FullMethodName = "/authd.UserService/ListGroups" @@ -410,6 +412,8 @@ type UserServiceClient interface { UnlockUser(ctx context.Context, in *UnlockUserRequest, opts ...grpc.CallOption) (*Empty, error) SetUserID(ctx context.Context, in *SetUserIDRequest, opts ...grpc.CallOption) (*SetUserIDResponse, error) SetGroupID(ctx context.Context, in *SetGroupIDRequest, opts ...grpc.CallOption) (*SetGroupIDResponse, error) + DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*Empty, error) + DeleteGroup(ctx context.Context, in *DeleteGroupRequest, opts ...grpc.CallOption) (*Empty, error) GetGroupByName(ctx context.Context, in *GetGroupByNameRequest, opts ...grpc.CallOption) (*Group, error) GetGroupByID(ctx context.Context, in *GetGroupByIDRequest, opts ...grpc.CallOption) (*Group, error) ListGroups(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Groups, error) @@ -493,6 +497,26 @@ func (c *userServiceClient) SetGroupID(ctx context.Context, in *SetGroupIDReques return out, nil } +func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, UserService_DeleteUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) DeleteGroup(ctx context.Context, in *DeleteGroupRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, UserService_DeleteGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *userServiceClient) GetGroupByName(ctx context.Context, in *GetGroupByNameRequest, opts ...grpc.CallOption) (*Group, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Group) @@ -534,6 +558,8 @@ type UserServiceServer interface { UnlockUser(context.Context, *UnlockUserRequest) (*Empty, error) SetUserID(context.Context, *SetUserIDRequest) (*SetUserIDResponse, error) SetGroupID(context.Context, *SetGroupIDRequest) (*SetGroupIDResponse, error) + DeleteUser(context.Context, *DeleteUserRequest) (*Empty, error) + DeleteGroup(context.Context, *DeleteGroupRequest) (*Empty, error) GetGroupByName(context.Context, *GetGroupByNameRequest) (*Group, error) GetGroupByID(context.Context, *GetGroupByIDRequest) (*Group, error) ListGroups(context.Context, *Empty) (*Groups, error) @@ -568,6 +594,12 @@ func (UnimplementedUserServiceServer) SetUserID(context.Context, *SetUserIDReque func (UnimplementedUserServiceServer) SetGroupID(context.Context, *SetGroupIDRequest) (*SetGroupIDResponse, error) { return nil, status.Error(codes.Unimplemented, "method SetGroupID not implemented") } +func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented") +} +func (UnimplementedUserServiceServer) DeleteGroup(context.Context, *DeleteGroupRequest) (*Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteGroup not implemented") +} func (UnimplementedUserServiceServer) GetGroupByName(context.Context, *GetGroupByNameRequest) (*Group, error) { return nil, status.Error(codes.Unimplemented, "method GetGroupByName not implemented") } @@ -724,6 +756,42 @@ func _UserService_SetGroupID_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).DeleteUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_DeleteUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_DeleteGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).DeleteGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_DeleteGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).DeleteGroup(ctx, req.(*DeleteGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _UserService_GetGroupByName_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetGroupByNameRequest) if err := dec(in); err != nil { @@ -813,6 +881,14 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetGroupID", Handler: _UserService_SetGroupID_Handler, }, + { + MethodName: "DeleteUser", + Handler: _UserService_DeleteUser_Handler, + }, + { + MethodName: "DeleteGroup", + Handler: _UserService_DeleteGroup_Handler, + }, { MethodName: "GetGroupByName", Handler: _UserService_GetGroupByName_Handler, diff --git a/internal/services/testdata/golden/TestRegisterGRPCServices b/internal/services/testdata/golden/TestRegisterGRPCServices index 871d7f2387..ecb10ed5ce 100644 --- a/internal/services/testdata/golden/TestRegisterGRPCServices +++ b/internal/services/testdata/golden/TestRegisterGRPCServices @@ -27,6 +27,12 @@ authd.PAM: metadata: authd.proto authd.UserService: methods: + - name: DeleteGroup + isclientstream: false + isserverstream: false + - name: DeleteUser + isclientstream: false + isserverstream: false - name: GetGroupByID isclientstream: false isserverstream: false diff --git a/internal/services/user/testdata/delete-user.db.yaml b/internal/services/user/testdata/delete-user.db.yaml new file mode 100644 index 0000000000..060df04dea --- /dev/null +++ b/internal/services/user/testdata/delete-user.db.yaml @@ -0,0 +1,60 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1@example.com + shell: /bin/bash + broker_id: "1902181170" + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: "1902181170" + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: "1902181170" + - name: delete_error@example.com + uid: 4444 + gid: 44444 + gecos: DeleteError + dir: /home/delete_error@example.com + shell: /bin/bash + broker_id: "1902181170" +groups: + - name: group1 + gid: 11111 + ugid: group1 + - name: group2 + gid: 22222 + ugid: group2 + - name: group3 + gid: 33333 + ugid: group3 + - name: group4 + gid: 44444 + ugid: group4 + - name: commongroup + gid: 99999 + ugid: commongroup +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 diff --git a/internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group b/internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group new file mode 100644 index 0000000000..db754b3127 --- /dev/null +++ b/internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group @@ -0,0 +1,42 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: broker-id +groups: + - name: group1 + gid: 11111 + ugid: group1 + - name: group2 + gid: 22222 + ugid: group2 + - name: group3 + gid: 33333 + ugid: group3 +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 3333 + gid: 33333 +schema_version: 2 diff --git a/internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group_with_uppercase b/internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group_with_uppercase new file mode 100644 index 0000000000..db754b3127 --- /dev/null +++ b/internal/services/user/testdata/golden/TestDeleteGroup/Successfully_delete_group_with_uppercase @@ -0,0 +1,42 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: broker-id +groups: + - name: group1 + gid: 11111 + ugid: group1 + - name: group2 + gid: 22222 + ugid: group2 + - name: group3 + gid: 33333 + ugid: group3 +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 3333 + gid: 33333 +schema_version: 2 diff --git a/internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user b/internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user new file mode 100644 index 0000000000..b7478b921b --- /dev/null +++ b/internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user @@ -0,0 +1,47 @@ +users: + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: "1902181170" + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: "1902181170" + - name: delete_error@example.com + uid: 4444 + gid: 44444 + gecos: DeleteError + dir: /home/delete_error@example.com + shell: /bin/bash + broker_id: "1902181170" +groups: + - name: group2 + gid: 22222 + ugid: group2 + - name: group3 + gid: 33333 + ugid: group3 + - name: group4 + gid: 44444 + ugid: group4 + - name: commongroup + gid: 99999 + ugid: commongroup +users_to_groups: + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 +schema_version: 2 diff --git a/internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user_with_uppercase b/internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user_with_uppercase new file mode 100644 index 0000000000..b7478b921b --- /dev/null +++ b/internal/services/user/testdata/golden/TestDeleteUser/Successfully_delete_user_with_uppercase @@ -0,0 +1,47 @@ +users: + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: "1902181170" + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: "1902181170" + - name: delete_error@example.com + uid: 4444 + gid: 44444 + gecos: DeleteError + dir: /home/delete_error@example.com + shell: /bin/bash + broker_id: "1902181170" +groups: + - name: group2 + gid: 22222 + ugid: group2 + - name: group3 + gid: 33333 + ugid: group3 + - name: group4 + gid: 44444 + ugid: group4 + - name: commongroup + gid: 99999 + ugid: commongroup +users_to_groups: + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 +schema_version: 2 diff --git a/internal/services/user/user.go b/internal/services/user/user.go index 4d976d8297..a027784308 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -270,6 +270,69 @@ func (s Service) SetGroupID(ctx context.Context, req *authd.SetGroupIDRequest) ( }, nil } +// DeleteUser removes the user with the given name from the authd database. +func (s Service) DeleteUser(ctx context.Context, req *authd.DeleteUserRequest) (*authd.Empty, error) { + if err := s.permissionManager.CheckRequestIsFromRoot(ctx); err != nil { + return nil, status.Error(codes.PermissionDenied, err.Error()) + } + + // authd uses lowercase usernames. + name := strings.ToLower(req.GetName()) + if name == "" { + return nil, status.Error(codes.InvalidArgument, "no user name provided") + } + + // Look up which broker owns this user before removing them from the DB. + brokerID, err := s.userManager.BrokerForUser(name) + if err != nil { + log.Errorf(ctx, "DeleteUser: could not determine broker for user %q: %v", name, err) + return nil, grpcError(err) + } + + if err := s.userManager.DeleteUser(name, req.GetRemoveHome()); err != nil { + log.Errorf(ctx, "DeleteUser: %v", err) + return nil, grpcError(err) + } + + // 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) + } + } + + return &authd.Empty{}, nil +} + +// DeleteGroup removes the group with the given name from the authd database. +func (s Service) DeleteGroup(ctx context.Context, req *authd.DeleteGroupRequest) (*authd.Empty, error) { + if err := s.permissionManager.CheckRequestIsFromRoot(ctx); err != nil { + return nil, status.Error(codes.PermissionDenied, err.Error()) + } + + // authd uses lowercase group names. + name := strings.ToLower(req.GetName()) + if name == "" { + return nil, status.Error(codes.InvalidArgument, "no group name provided") + } + + if err := s.userManager.DeleteGroup(name); err != nil { + log.Errorf(ctx, "DeleteGroup: %v", err) + return nil, grpcError(err) + } + + return &authd.Empty{}, nil +} + // userToProtobuf converts a types.UserEntry to authd.User. func userToProtobuf(u types.UserEntry) *authd.User { return &authd.User{ @@ -344,12 +407,17 @@ func (s Service) userPreCheck(ctx context.Context, username string) (types.UserE return u, nil } -// grpcError converts a data not found to proper GRPC status code. -// The NSS module uses this status code to determine the NSS status it should return. +// grpcError converts well-known user manager errors to their proper gRPC status codes. +// The NSS module uses these status codes to determine the NSS status it should return. func grpcError(err error) error { if errors.Is(err, users.NoDataFoundError{}) { return status.Error(codes.NotFound, err.Error()) } + var primaryErr users.GroupIsPrimaryError + if errors.As(err, &primaryErr) { + return status.Error(codes.FailedPrecondition, err.Error()) + } + return err } diff --git a/internal/services/user/user_test.go b/internal/services/user/user_test.go index d66721419b..71c1b2cf3f 100644 --- a/internal/services/user/user_test.go +++ b/internal/services/user/user_test.go @@ -423,6 +423,85 @@ func TestSetGroupID(t *testing.T) { } } +func TestDeleteUser(t *testing.T) { + tests := map[string]struct { + sourceDB string + username string + currentUserNotRoot bool + + wantErr bool + }{ + "Successfully_delete_user": {username: "user1@example.com"}, + "Successfully_delete_user_with_uppercase": {username: "USER1@EXAMPLE.COM"}, + + "Error_when_username_is_empty": {wantErr: true}, + "Error_when_user_does_not_exist": {username: "doesnotexist@example.com", wantErr: true}, + "Error_when_not_root": {username: "user1@example.com", currentUserNotRoot: true, wantErr: true}, + "Error_when_broker_fails_to_delete": {username: "delete_error@example.com", wantErr: true}, + "Error_when_broker_not_found": {sourceDB: "default.db.yaml", username: "user1@example.com", wantErr: true}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if !tc.wantErr { + userslocking.Z_ForTests_OverrideLockingWithCleanup(t) + } + + dbFile := tc.sourceDB + if dbFile == "" { + dbFile = "delete-user.db.yaml" + } + + client, m := newUserServiceClient(t, dbFile, tc.currentUserNotRoot) + + _, err := client.DeleteUser(context.Background(), &authd.DeleteUserRequest{Name: tc.username}) + if tc.wantErr { + require.Error(t, err, "DeleteUser should return an error, but did not") + return + } + require.NoError(t, err, "DeleteUser should not return an error, but did") + + dbContent, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.DBManager(m)) + require.NoError(t, err, "Setup: failed to dump database for comparing") + golden.CheckOrUpdate(t, dbContent) + }) + } +} + +func TestDeleteGroup(t *testing.T) { + tests := map[string]struct { + sourceDB string + + groupname string + currentUserNotRoot bool + + wantErr bool + }{ + "Successfully_delete_group": {groupname: "commongroup"}, + "Successfully_delete_group_with_uppercase": {groupname: "COMMONGROUP"}, + + "Error_when_groupname_is_empty": {wantErr: true}, + "Error_when_group_does_not_exist": {groupname: "doesnotexist", wantErr: true}, + "Error_when_not_root": {groupname: "commongroup", currentUserNotRoot: true, wantErr: true}, + "Error_when_group_is_primary_group_of_an_existing_user": {groupname: "group1", wantErr: true}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + client, m := newUserServiceClient(t, tc.sourceDB, tc.currentUserNotRoot) + + _, err := client.DeleteGroup(context.Background(), &authd.DeleteGroupRequest{Name: tc.groupname}) + if tc.wantErr { + require.Error(t, err, "DeleteGroup should return an error, but did not") + return + } + require.NoError(t, err, "DeleteGroup should not return an error, but did") + + dbContent, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.DBManager(m)) + require.NoError(t, err, "Setup: failed to dump database for comparing") + golden.CheckOrUpdate(t, dbContent) + }) + } +} + // newUserServiceClient returns a new gRPC client for the CLI service. func newUserServiceClient(t *testing.T, dbFile string, currentUserNotRoot ...bool) (client authd.UserServiceClient, userManager *users.Manager) { t.Helper() diff --git a/internal/testutils/broker.go b/internal/testutils/broker.go index 3ef9eb3d26..8a21baa3ba 100644 --- a/internal/testutils/broker.go +++ b/internal/testutils/broker.go @@ -349,6 +349,14 @@ func (b *BrokerBusMock) UserPreCheck(username string) (userinfo string, dbusErr return userInfoFromName(username, nil), nil } +// DeleteUser removes broker side user data or returns an error if requested. +func (b *BrokerBusMock) DeleteUser(username string) (dbusErr *dbus.Error) { + if strings.Contains(username, "delete_error") { + return dbus.MakeFailedError(fmt.Errorf("broker %q: DeleteUser errored out", b.name)) + } + return nil +} + // parseSessionID is wrapper around the sessionID to remove some values appended during the tests. // // The sessionID can have multiple values appended to differentiate between subtests and avoid concurrency conflicts, diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index 47cce2060f..3fcf52cdb8 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -1066,6 +1066,46 @@ func TestDeleteUser(t *testing.T) { } } +func TestDeleteGroup(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + dbFile string + gid uint32 + + wantErr bool + wantErrType error + }{ + "Deleting_sole_group_of_a_user_removes_group_and_memberships_but_keeps_user": {dbFile: "one_user_and_group", gid: 11111}, + "Deleting_shared_group_removes_only_that_group_and_its_memberships": {dbFile: "multiple_users_and_groups", gid: 99999}, + + "Error_on_nonexistent_group": {gid: 11111, wantErrType: db.NoDataFoundError{}}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + c := initDB(t, tc.dbFile) + + err := c.DeleteGroup(tc.gid) + log.Debugf(context.Background(), "DeleteGroup error: %v", err) + if tc.wantErr { + require.Error(t, err, "DeleteGroup should return an error but didn't") + return + } + if tc.wantErrType != nil { + require.ErrorIs(t, err, tc.wantErrType, "DeleteGroup should return expected error") + return + } + require.NoError(t, err) + + got, err := db.Z_ForTests_DumpNormalizedYAML(c) + require.NoError(t, err) + golden.CheckOrUpdate(t, got) + }) + } +} + // TestBackwardCompatibilityAndMigrations covers loading legacy schemas (e.g., v2 with INT ugid) // and migrating older schemas (e.g., v1 without 'locked' column) to the latest schema. func TestBackwardCompatibilityAndMigrations(t *testing.T) { diff --git a/internal/users/db/groups.go b/internal/users/db/groups.go index 0382e6f1dd..946ae4870e 100644 --- a/internal/users/db/groups.go +++ b/internal/users/db/groups.go @@ -260,3 +260,24 @@ func updateGroupByID(db queryable, g GroupRow) error { return nil } + +// DeleteGroup removes the group from the database. +func (m *Manager) DeleteGroup(gid uint32) error { + m.mu.Lock() + defer m.mu.Unlock() + + query := `DELETE FROM groups WHERE gid = ?` + res, err := m.db.Exec(query, gid) + if err != nil { + return fmt.Errorf("failed to delete group: %w", err) + } + rowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + if rowsAffected == 0 { + return NewGIDNotFoundError(gid) + } + + return nil +} diff --git a/internal/users/db/testdata/golden/TestDeleteGroup/Deleting_shared_group_removes_only_that_group_and_its_memberships b/internal/users/db/testdata/golden/TestDeleteGroup/Deleting_shared_group_removes_only_that_group_and_its_memberships new file mode 100644 index 0000000000..c6c7a3d86e --- /dev/null +++ b/internal/users/db/testdata/golden/TestDeleteGroup/Deleting_shared_group_removes_only_that_group_and_its_memberships @@ -0,0 +1,53 @@ +users: + - name: user1 + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1 + shell: /bin/bash + broker_id: broker-id + - name: user2 + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2 + shell: /bin/dash + broker_id: broker-id + - name: user3 + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3 + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /home/userwithoutbroker + shell: /bin/sh +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2 + gid: 22222 + ugid: "56781234" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 3333 + gid: 33333 + - uid: 4444 + gid: 44444 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestDeleteGroup/Deleting_sole_group_of_a_user_removes_group_and_memberships_but_keeps_user b/internal/users/db/testdata/golden/TestDeleteGroup/Deleting_sole_group_of_a_user_removes_group_and_memberships_but_keeps_user new file mode 100644 index 0000000000..b0fcedfa5d --- /dev/null +++ b/internal/users/db/testdata/golden/TestDeleteGroup/Deleting_sole_group_of_a_user_removes_group_and_memberships_but_keeps_user @@ -0,0 +1,13 @@ +users: + - name: user1 + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1 + shell: /bin/bash + broker_id: broker-id +groups: [] +users_to_groups: [] +schema_version: 2 diff --git a/internal/users/db/users.go b/internal/users/db/users.go index 60efe3d204..0723cf982b 100644 --- a/internal/users/db/users.go +++ b/internal/users/db/users.go @@ -191,6 +191,36 @@ func (m *Manager) DeleteUser(uid uint32) error { return nil } +// UsersWithPrimaryGroup returns all users whose primary GID matches the given GID. +func (m *Manager) UsersWithPrimaryGroup(gid uint32) ([]UserRow, error) { + return usersWithPrimaryGroup(m.db, gid) +} + +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) + + var users []UserRow + for rows.Next() { + var u UserRow + err := rows.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Locked) + if err != nil { + return nil, fmt.Errorf("scan error: %w", err) + } + users = append(users, u) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return users, nil +} + // UserWithGroups returns a user and their groups, including local groups, in a single transaction. func (m *Manager) UserWithGroups(name string) (u UserRow, groups []GroupRow, localGroups []string, err error) { // Start a transaction diff --git a/internal/users/defs.go b/internal/users/defs.go index 786c9e1fe5..6f8025e46c 100644 --- a/internal/users/defs.go +++ b/internal/users/defs.go @@ -1,6 +1,9 @@ package users import ( + "fmt" + "strings" + "github.com/canonical/authd/internal/sliceutils" "github.com/canonical/authd/internal/users/db" "github.com/canonical/authd/internal/users/types" @@ -68,3 +71,15 @@ func groupEntryFromGroupWithMembers(g db.GroupWithMembers) types.GroupEntry { // NoDataFoundError is the error returned when no entry is found in the db. type NoDataFoundError = db.NoDataFoundError + +// GroupIsPrimaryError is returned when trying to delete a group that is still +// the primary group of one or more users. +type GroupIsPrimaryError struct { + GroupName string + Users []string +} + +// Error implements the error interface for GroupIsPrimaryError. +func (e GroupIsPrimaryError) Error() string { + return fmt.Sprintf("group %q is the primary group of user(s): %s", e.GroupName, strings.Join(e.Users, ", ")) +} diff --git a/internal/users/export_test.go b/internal/users/export_test.go index 3d4673af8a..f69727f795 100644 --- a/internal/users/export_test.go +++ b/internal/users/export_test.go @@ -22,6 +22,10 @@ func CompareNewUserInfoWithUserInfoFromDB(newUserInfo, dbUserInfo types.UserInfo return compareNewUserInfoWithUserInfoFromDB(newUserInfo, dbUserInfo) } +func (m *Manager) IsGroupPrimaryForUsers(gid uint32) ([]string, error) { + return m.isGroupPrimaryForUsers(gid) +} + const ( SystemdDynamicUIDMin = systemdDynamicUIDMin SystemdDynamicUIDMax = systemdDynamicUIDMax diff --git a/internal/users/manager.go b/internal/users/manager.go index 3b978de19b..713d4f1e1d 100644 --- a/internal/users/manager.go +++ b/internal/users/manager.go @@ -702,6 +702,86 @@ func (m *Manager) UnlockUser(username string) error { return nil } +// DeleteUser removes the user with the given name from the database. +// If removeHome is true, the user's home directory is also removed. +func (m *Manager) DeleteUser(username string, removeHome bool) error { + m.userManagementMu.Lock() + defer m.userManagementMu.Unlock() + + userRow, err := m.db.UserByName(username) + if err != nil { + return err + } + + lockedEntries, unlockEntries, err := localentries.WithUserDBLock() + if err != nil { + return err + } + defer func() { err = errors.Join(err, unlockEntries()) }() + + // Remove the user from any local groups they are a member of. + _, _, localGroups, err := m.db.UserWithGroups(username) + if err != nil { + return err + } + if err := localentries.UpdateGroups(lockedEntries, username, nil, localGroups); err != nil { + return err + } + + if err := m.db.DeleteUser(userRow.UID); err != nil { + return err + } + + // Delete the user's primary group + if err := m.db.DeleteGroup(userRow.GID); err != nil { + return fmt.Errorf("failed to delete primary group for user %q: %w", username, err) + } + + if removeHome && userRow.Dir != "" { + if err := os.RemoveAll(userRow.Dir); err != nil { + return fmt.Errorf("failed to remove home directory %q for user %q: %w", userRow.Dir, username, err) + } + } + + return nil +} + +// isGroupPrimaryForUsers returns the names of users for which the given GID is +// their primary group. It returns an empty slice when no such users exist. +func (m *Manager) isGroupPrimaryForUsers(gid uint32) ([]string, error) { + primaryUsers, err := m.db.UsersWithPrimaryGroup(gid) + if err != nil { + return nil, err + } + + names := make([]string, 0, len(primaryUsers)) + for _, u := range primaryUsers { + names = append(names, u.Name) + } + return names, nil +} + +// DeleteGroup removes the group with the given name from the database. +func (m *Manager) DeleteGroup(groupname string) error { + m.userManagementMu.Lock() + defer m.userManagementMu.Unlock() + + groupRow, err := m.db.GroupByName(groupname) + if err != nil { + return err + } + + primaryUserNames, err := m.isGroupPrimaryForUsers(groupRow.GID) + if err != nil { + return fmt.Errorf("failed to check for users with primary group %q: %w", groupname, err) + } + if len(primaryUserNames) > 0 { + return GroupIsPrimaryError{GroupName: groupname, Users: primaryUserNames} + } + + return m.db.DeleteGroup(groupRow.GID) +} + // IsUserLocked returns true if the user with the given user name is locked, false otherwise. func (m *Manager) IsUserLocked(username string) (bool, error) { u, err := m.db.UserByName(username) diff --git a/internal/users/manager_test.go b/internal/users/manager_test.go index decb993776..8d72957356 100644 --- a/internal/users/manager_test.go +++ b/internal/users/manager_test.go @@ -795,6 +795,153 @@ func TestUpdateBrokerForUser(t *testing.T) { } } +func TestDeleteUser(t *testing.T) { + tests := map[string]struct { + username string + + localGroupsFile string + removeHome bool + + wantErr bool + wantErrType error + }{ + "Successfully_delete_user": {}, + "Successfully_delete_user_removes_them_from_local_groups": {localGroupsFile: "users_in_groups.group"}, + "Successfully_delete_user_keeps_other_users_in_shared_group": {username: "user2@example.com"}, + "Successfully_delete_user_and_remove_home": {removeHome: true}, + + "Error_if_user_does_not_exist": {username: "doesnotexist@example.com", wantErrType: db.NoDataFoundError{}}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + groupFile := tc.localGroupsFile + if tc.localGroupsFile == "" { + groupFile = "empty.group" + } + destGroupFile := localgroupstestutils.SetupGroupMock(t, filepath.Join("testdata", "groups", groupFile)) + + if tc.username == "" { + tc.username = "user1@example.com" + } + + dbDir := t.TempDir() + dbFile := "multiple_users_and_groups_with_tmp_home" + err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", "db", dbFile+".db.yaml"), dbDir) + require.NoError(t, err, "Setup: could not create database from testdata") + m := newManagerForTests(t, dbDir) + + var userHome string + if tc.username != "doesnotexist@example.com" { + user, err := m.UserByName(tc.username) + require.NoError(t, err, "Setup: could not look up user") + userHome = user.Dir + if userHome != "" { + err = os.MkdirAll(userHome, 0o700) + require.NoError(t, err, "Setup: could not create home directory for %s", tc.username) + } + } + // We expect db file to have user home directories under + // /tmp/authd-delete-user-test to keep the cleanup logic simple + t.Cleanup(func() { _ = os.RemoveAll("/tmp/authd-delete-user-test/") }) + + err = m.DeleteUser(tc.username, tc.removeHome) + log.Debugf(context.Background(), "DeleteUser error: %v", err) + + requireErrorAssertions(t, err, tc.wantErrType, tc.wantErr) + if tc.wantErrType != nil || tc.wantErr { + return + } + + if tc.removeHome { + require.NoDirExists(t, userHome, "Home directory should have been removed") + } else { + require.DirExists(t, userHome, "Home directory should still exist") + } + + got, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.DBManager(m)) + require.NoError(t, err, "Created database should be valid yaml content") + + golden.CheckOrUpdate(t, got) + + localgroupstestutils.RequireGroupFile(t, destGroupFile, golden.Path(t)) + }) + } +} + +func TestDeleteGroup(t *testing.T) { + tests := map[string]struct { + groupname string + dbFile string + + wantErr bool + wantErrType error + }{ + "Successfully_delete_group_keeps_its_members_in_the_db": {groupname: "nonprimarygroup", dbFile: "multiple_users_and_groups_with_non_primary_group"}, + "Successfully_delete_shared_group_leaves_other_groups_intact": {groupname: "commongroup", dbFile: "multiple_users_and_groups"}, + + "Error_if_group_does_not_exist": {groupname: "doesnotexist", dbFile: "multiple_users_and_groups", wantErrType: db.NoDataFoundError{}}, + "Error_if_group_is_primary_group_of_an_existing_user": {groupname: "group1", dbFile: "multiple_users_and_groups", wantErr: true}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // We don't care about the output of gpasswd in this test, but we still need to mock it. + _ = localgroupstestutils.SetupGroupMock(t, filepath.Join("testdata", "groups", "empty.group")) + + dbDir := t.TempDir() + err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", "db", tc.dbFile+".db.yaml"), dbDir) + require.NoError(t, err, "Setup: could not create database from testdata") + m := newManagerForTests(t, dbDir) + + err = m.DeleteGroup(tc.groupname) + log.Debugf(context.Background(), "DeleteGroup error: %v", err) + + requireErrorAssertions(t, err, tc.wantErrType, tc.wantErr) + if tc.wantErrType != nil || tc.wantErr { + return + } + + got, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.DBManager(m)) + require.NoError(t, err, "Created database should be valid yaml content") + + golden.CheckOrUpdate(t, got) + }) + } +} + +func TestIsGroupPrimaryForUsers(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + gid uint32 + + wantErr bool + }{ + "Returns_users_for_which_the_group_is_primary": {gid: 11111}, + "Returns_empty_slice_when_no_user_has_it_as_primary": {gid: 88888}, + "Returns_empty_slice_for_shared_group_that_is_not_primary": {gid: 99999}, + "Returns_multiple_users_when_group_is_primary_for_several": {gid: 55555}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + dbDir := t.TempDir() + err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", "db", "group_primary_check.db.yaml"), dbDir) + require.NoError(t, err, "Setup: could not create database from testdata") + m := newManagerForTests(t, dbDir) + + got, err := m.IsGroupPrimaryForUsers(tc.gid) + + requireErrorAssertions(t, err, nil, tc.wantErr) + if tc.wantErr { + return + } + + golden.CheckOrUpdateYAML(t, got) + }) + } +} + //nolint:dupl // This is not a duplicate test func TestLockUser(t *testing.T) { tests := map[string]struct { diff --git a/internal/users/testdata/db/group_primary_check.db.yaml b/internal/users/testdata/db/group_primary_check.db.yaml new file mode 100644 index 0000000000..7d0e305f05 --- /dev/null +++ b/internal/users/testdata/db/group_primary_check.db.yaml @@ -0,0 +1,64 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: User1 + dir: /home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 55555 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: user4@example.com + uid: 4444 + gid: 55555 + gecos: User4 + dir: /home/user4@example.com + shell: /bin/sh + broker_id: broker-id +groups: + - name: group1 + gid: 11111 + ugid: "11111111" + - name: group2 + gid: 22222 + ugid: "22222222" + - name: sharedprimarygroup + gid: 55555 + ugid: "55555555" + - name: nonprimarygroup + gid: 88888 + ugid: "88888888" + - name: commongroup + gid: 99999 + ugid: "99999999" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 1111 + gid: 88888 + - uid: 1111 + gid: 99999 + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 55555 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 55555 + - uid: 4444 + gid: 99999 diff --git a/internal/users/testdata/db/multiple_users_and_groups_with_non_primary_group.db.yaml b/internal/users/testdata/db/multiple_users_and_groups_with_non_primary_group.db.yaml new file mode 100644 index 0000000000..6cafea3154 --- /dev/null +++ b/internal/users/testdata/db/multiple_users_and_groups_with_non_primary_group.db.yaml @@ -0,0 +1,68 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" + - name: nonprimarygroup + gid: 88888 + ugid: "88888888" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 1111 + gid: 88888 + - uid: 1111 + gid: 99999 + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 diff --git a/internal/users/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml b/internal/users/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml new file mode 100644 index 0000000000..3b6191a2b5 --- /dev/null +++ b/internal/users/testdata/db/multiple_users_and_groups_with_tmp_home.db.yaml @@ -0,0 +1,63 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /tmp/authd-delete-user-test/home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /tmp/authd-delete-user-test/home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /tmp/authd-delete-user-test/home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /tmp/authd-delete-user-test/home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 1111 + gid: 99999 + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 diff --git a/internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_group_keeps_its_members_in_the_db b/internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_group_keeps_its_members_in_the_db new file mode 100644 index 0000000000..9009ddf20c --- /dev/null +++ b/internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_group_keeps_its_members_in_the_db @@ -0,0 +1,64 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 1111 + gid: 99999 + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_shared_group_leaves_other_groups_intact b/internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_shared_group_leaves_other_groups_intact new file mode 100644 index 0000000000..5841fbf88e --- /dev/null +++ b/internal/users/testdata/golden/TestDeleteGroup/Successfully_delete_shared_group_leaves_other_groups_intact @@ -0,0 +1,53 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 3333 + gid: 33333 + - uid: 4444 + gid: 44444 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user new file mode 100644 index 0000000000..7161e14804 --- /dev/null +++ b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user @@ -0,0 +1,48 @@ +users: + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /tmp/authd-delete-user-test/home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /tmp/authd-delete-user-test/home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /tmp/authd-delete-user-test/home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" +users_to_groups: + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_and_remove_home b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_and_remove_home new file mode 100644 index 0000000000..7161e14804 --- /dev/null +++ b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_and_remove_home @@ -0,0 +1,48 @@ +users: + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /tmp/authd-delete-user-test/home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /tmp/authd-delete-user-test/home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /tmp/authd-delete-user-test/home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" +users_to_groups: + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_keeps_other_users_in_shared_group b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_keeps_other_users_in_shared_group new file mode 100644 index 0000000000..f67ee1d5fd --- /dev/null +++ b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_keeps_other_users_in_shared_group @@ -0,0 +1,50 @@ +users: + - name: user1@example.com + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /tmp/authd-delete-user-test/home/user1@example.com + shell: /bin/bash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /tmp/authd-delete-user-test/home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /tmp/authd-delete-user-test/home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 1111 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_removes_them_from_local_groups b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_removes_them_from_local_groups new file mode 100644 index 0000000000..7161e14804 --- /dev/null +++ b/internal/users/testdata/golden/TestDeleteUser/Successfully_delete_user_removes_them_from_local_groups @@ -0,0 +1,48 @@ +users: + - name: user2@example.com + uid: 2222 + gid: 22222 + gecos: User2 + dir: /tmp/authd-delete-user-test/home/user2@example.com + shell: /bin/dash + broker_id: broker-id + - name: user3@example.com + uid: 3333 + gid: 33333 + gecos: User3 + dir: /tmp/authd-delete-user-test/home/user3@example.com + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker@example.com + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /tmp/authd-delete-user-test/home/userwithoutbroker@example.com + shell: /bin/sh +groups: + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" +users_to_groups: + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_for_shared_group_that_is_not_primary b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_for_shared_group_that_is_not_primary new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_for_shared_group_that_is_not_primary @@ -0,0 +1 @@ +[] diff --git a/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_when_no_user_has_it_as_primary b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_when_no_user_has_it_as_primary new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_empty_slice_when_no_user_has_it_as_primary @@ -0,0 +1 @@ +[] diff --git a/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_multiple_users_when_group_is_primary_for_several b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_multiple_users_when_group_is_primary_for_several new file mode 100644 index 0000000000..4534eb0ad6 --- /dev/null +++ b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_multiple_users_when_group_is_primary_for_several @@ -0,0 +1,2 @@ +- user3@example.com +- user4@example.com diff --git a/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_users_for_which_the_group_is_primary b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_users_for_which_the_group_is_primary new file mode 100644 index 0000000000..eb8908e923 --- /dev/null +++ b/internal/users/testdata/golden/TestIsGroupPrimaryForUsers/Returns_users_for_which_the_group_is_primary @@ -0,0 +1 @@ +- user1@example.com