Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions cmd/authctl/group/delete.go
Original file line number Diff line number Diff line change
@@ -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 <group>",
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
}
98 changes: 98 additions & 0 deletions cmd/authctl/group/delete_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
1 change: 1 addition & 0 deletions cmd/authctl/group/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ var GroupCmd = &cobra.Command{

func init() {
GroupCmd.AddCommand(setGIDCmd)
GroupCmd.AddCommand(deleteGroupCmd)
}
43 changes: 43 additions & 0 deletions cmd/authctl/group/testdata/db/multiple_users_and_groups.db.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Group "group3-nonprimary" has been deleted from the authd database.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: connection error: desc = "transport: Error while dialing: dial unix /non-existent: connect: no such file or directory"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: group "nonexistent" not found
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: group "group1" is the primary group of user(s): user1@example.com
86 changes: 86 additions & 0 deletions cmd/authctl/user/delete.go
Original file line number Diff line number Diff line change
@@ -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 <user>",
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
}
Loading
Loading