From 2e5abf640af0a07ef9cc912d9a64e4ae1267ee67 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Fri, 6 Oct 2023 19:32:57 +0000 Subject: [PATCH] feat(cli): add `coder users delete` command --- cli/testdata/coder_users_--help.golden | 1 + cli/testdata/coder_users_delete_--help.golden | 11 ++ .../coder_users_suspend_--help.golden | 2 - cli/user_delete_test.go | 165 ++++++++++++++++++ cli/userdelete.go | 42 +++++ cli/users.go | 1 + cli/userstatus.go | 1 - coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/users.go | 2 +- docs/api/users.md | 6 +- docs/cli/users.md | 1 + docs/cli/users_delete.md | 15 ++ docs/cli/users_suspend.md | 5 - docs/manifest.json | 5 + 15 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 cli/testdata/coder_users_delete_--help.golden create mode 100644 cli/user_delete_test.go create mode 100644 cli/userdelete.go create mode 100644 docs/cli/users_delete.md diff --git a/cli/testdata/coder_users_--help.golden b/cli/testdata/coder_users_--help.golden index 4a849a871daf3..338fea4febc86 100644 --- a/cli/testdata/coder_users_--help.golden +++ b/cli/testdata/coder_users_--help.golden @@ -11,6 +11,7 @@ SUBCOMMANDS: activate Update a user's status to 'active'. Active users can fully interact with the platform create + delete Delete a user by username or user_id. list show Show a single user. Use 'me' to indicate the currently authenticated user. diff --git a/cli/testdata/coder_users_delete_--help.golden b/cli/testdata/coder_users_delete_--help.golden new file mode 100644 index 0000000000000..a8494101ab6d5 --- /dev/null +++ b/cli/testdata/coder_users_delete_--help.golden @@ -0,0 +1,11 @@ +coder v0.0.0-devel + +USAGE: + coder users delete + + Delete a user by username or user_id. + + Aliases: rm + +——— +Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_users_suspend_--help.golden b/cli/testdata/coder_users_suspend_--help.golden index 99c621e7d6a3a..8b706e92d6d7a 100644 --- a/cli/testdata/coder_users_suspend_--help.golden +++ b/cli/testdata/coder_users_suspend_--help.golden @@ -6,8 +6,6 @@ USAGE: Update a user's status to 'suspended'. A suspended user cannot log into the platform - Aliases: rm, delete - $ coder users suspend example_user OPTIONS: diff --git a/cli/user_delete_test.go b/cli/user_delete_test.go new file mode 100644 index 0000000000000..b1735ca668cc7 --- /dev/null +++ b/cli/user_delete_test.go @@ -0,0 +1,165 @@ +package cli_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/cryptorand" + "github.com/coder/coder/v2/pty/ptytest" +) + +func TestUserDelete(t *testing.T) { + t.Parallel() + t.Run("Username", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + client := coderdtest.New(t, nil) + aUser := coderdtest.CreateFirstUser(t, client) + + pw, err := cryptorand.String(16) + require.NoError(t, err) + + _, err = client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "colin5@coder.com", + Username: "coolin", + Password: pw, + UserLoginType: codersdk.LoginTypePassword, + OrganizationID: aUser.OrganizationID, + DisableLogin: false, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, "users", "delete", "coolin") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + errC := make(chan error) + go func() { + errC <- inv.Run() + }() + require.NoError(t, <-errC) + pty.ExpectMatch("coolin") + }) + + t.Run("UserID", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + client := coderdtest.New(t, nil) + aUser := coderdtest.CreateFirstUser(t, client) + + pw, err := cryptorand.String(16) + require.NoError(t, err) + + user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "colin5@coder.com", + Username: "coolin", + Password: pw, + UserLoginType: codersdk.LoginTypePassword, + OrganizationID: aUser.OrganizationID, + DisableLogin: false, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, "users", "delete", user.ID.String()) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + errC := make(chan error) + go func() { + errC <- inv.Run() + }() + require.NoError(t, <-errC) + pty.ExpectMatch("coolin") + }) + + t.Run("UserID", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + client := coderdtest.New(t, nil) + aUser := coderdtest.CreateFirstUser(t, client) + + pw, err := cryptorand.String(16) + require.NoError(t, err) + + user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "colin5@coder.com", + Username: "coolin", + Password: pw, + UserLoginType: codersdk.LoginTypePassword, + OrganizationID: aUser.OrganizationID, + DisableLogin: false, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, "users", "delete", user.ID.String()) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + errC := make(chan error) + go func() { + errC <- inv.Run() + }() + require.NoError(t, <-errC) + pty.ExpectMatch("coolin") + }) + + // TODO: reenable this test case. Fetching users without perms returns a + // "user "testuser@coder.com" must be a member of at least one organization" + // error. + // t.Run("NoPerms", func(t *testing.T) { + // t.Parallel() + // ctx := context.Background() + // client := coderdtest.New(t, nil) + // aUser := coderdtest.CreateFirstUser(t, client) + + // pw, err := cryptorand.String(16) + // require.NoError(t, err) + + // fmt.Println(aUser.OrganizationID) + // toDelete, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + // Email: "colin5@coder.com", + // Username: "coolin", + // Password: pw, + // UserLoginType: codersdk.LoginTypePassword, + // OrganizationID: aUser.OrganizationID, + // DisableLogin: false, + // }) + // require.NoError(t, err) + + // uClient, _ := coderdtest.CreateAnotherUser(t, client, aUser.OrganizationID) + // _ = uClient + // _ = toDelete + + // inv, root := clitest.New(t, "users", "delete", "coolin") + // clitest.SetupConfig(t, uClient, root) + // require.ErrorContains(t, inv.Run(), "...") + // }) + + t.Run("DeleteSelf", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + client := coderdtest.New(t, nil) + aUser := coderdtest.CreateFirstUser(t, client) + + pw, err := cryptorand.String(16) + require.NoError(t, err) + + _, err = client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "colin5@coder.com", + Username: "coolin", + Password: pw, + UserLoginType: codersdk.LoginTypePassword, + OrganizationID: aUser.OrganizationID, + DisableLogin: false, + }) + require.NoError(t, err) + + coderdtest.CreateAnotherUser(t, client, aUser.OrganizationID) + + inv, root := clitest.New(t, "users", "delete", "me") + clitest.SetupConfig(t, client, root) + require.ErrorContains(t, inv.Run(), "You cannot delete yourself!") + }) +} diff --git a/cli/userdelete.go b/cli/userdelete.go new file mode 100644 index 0000000000000..aeafc3bfa00d2 --- /dev/null +++ b/cli/userdelete.go @@ -0,0 +1,42 @@ +package cli + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/clibase" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" +) + +func (r *RootCmd) userDelete() *clibase.Cmd { + client := new(codersdk.Client) + cmd := &clibase.Cmd{ + Use: "delete ", + Short: "Delete a user by username or user_id.", + Middleware: clibase.Chain( + clibase.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(inv *clibase.Invocation) error { + ctx := inv.Context() + user, err := client.User(ctx, inv.Args[0]) + if err != nil { + return xerrors.Errorf("fetch user: %w", err) + } + + err = client.DeleteUser(ctx, user.ID) + if err != nil { + return xerrors.Errorf("delete user: %w", err) + } + + _, _ = fmt.Fprintln(inv.Stderr, + "Successfully deleted "+pretty.Sprint(cliui.DefaultStyles.Keyword, user.Username)+".", + ) + return nil + }, + } + return cmd +} diff --git a/cli/users.go b/cli/users.go index 92d79635cf1ba..160a17b77fa4a 100644 --- a/cli/users.go +++ b/cli/users.go @@ -17,6 +17,7 @@ func (r *RootCmd) users() *clibase.Cmd { r.userCreate(), r.userList(), r.userSingle(), + r.userDelete(), r.createUserStatusCommand(codersdk.UserStatusActive), r.createUserStatusCommand(codersdk.UserStatusSuspended), }, diff --git a/cli/userstatus.go b/cli/userstatus.go index 5f516e196b365..7590626cfbf44 100644 --- a/cli/userstatus.go +++ b/cli/userstatus.go @@ -28,7 +28,6 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibas case codersdk.UserStatusSuspended: verb = "suspend" pastVerb = "suspended" - aliases = []string{"rm", "delete"} short = "Update a user's status to 'suspended'. A suspended user cannot log into the platform" default: panic(fmt.Sprintf("%s is not supported", sdkStatus)) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7569827209d16..bb7dd87636cbb 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3373,7 +3373,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "User ID, name, or me", + "description": "User ID, username, or me", "name": "user", "in": "path", "required": true diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 29d3c3588033f..9e26d94e0023f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2961,7 +2961,7 @@ "parameters": [ { "type": "string", - "description": "User ID, name, or me", + "description": "User ID, username, or me", "name": "user", "in": "path", "required": true diff --git a/coderd/users.go b/coderd/users.go index 64b80c1677f95..7633290a80edf 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -501,7 +501,7 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Produce json // @Tags Users -// @Param user path string true "User ID, name, or me" +// @Param user path string true "User ID, username, or me" // @Success 200 {object} codersdk.User // @Router /users/{user} [get] func (api *API) userByName(rw http.ResponseWriter, r *http.Request) { diff --git a/docs/api/users.md b/docs/api/users.md index fdeed691da48f..1ea652b3ab2ef 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -346,9 +346,9 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \ ### Parameters -| Name | In | Type | Required | Description | -| ------ | ---- | ------ | -------- | -------------------- | -| `user` | path | string | true | User ID, name, or me | +| Name | In | Type | Required | Description | +| ------ | ---- | ------ | -------- | ------------------------ | +| `user` | path | string | true | User ID, username, or me | ### Example responses diff --git a/docs/cli/users.md b/docs/cli/users.md index ade49b04a866b..f0ca83cd93f2a 100644 --- a/docs/cli/users.md +++ b/docs/cli/users.md @@ -20,6 +20,7 @@ coder users [subcommand] | -------------------------------------------- | ------------------------------------------------------------------------------------- | | [activate](./users_activate.md) | Update a user's status to 'active'. Active users can fully interact with the platform | | [create](./users_create.md) | | +| [delete](./users_delete.md) | Delete a user by username or user_id. | | [list](./users_list.md) | | | [show](./users_show.md) | Show a single user. Use 'me' to indicate the currently authenticated user. | | [suspend](./users_suspend.md) | Update a user's status to 'suspended'. A suspended user cannot log into the platform | diff --git a/docs/cli/users_delete.md b/docs/cli/users_delete.md new file mode 100644 index 0000000000000..d4da1c8b5db7a --- /dev/null +++ b/docs/cli/users_delete.md @@ -0,0 +1,15 @@ + + +# users delete + +Delete a user by username or user_id. + +Aliases: + +- rm + +## Usage + +```console +coder users delete +``` diff --git a/docs/cli/users_suspend.md b/docs/cli/users_suspend.md index 20c0a3713c650..d2980d00a4d62 100644 --- a/docs/cli/users_suspend.md +++ b/docs/cli/users_suspend.md @@ -4,11 +4,6 @@ Update a user's status to 'suspended'. A suspended user cannot log into the platform -Aliases: - -- rm -- delete - ## Usage ```console diff --git a/docs/manifest.json b/docs/manifest.json index e6541f5634250..c154a44606c81 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -885,6 +885,11 @@ "title": "users create", "path": "cli/users_create.md" }, + { + "title": "users delete", + "description": "Delete a user by username or user_id.", + "path": "cli/users_delete.md" + }, { "title": "users list", "path": "cli/users_list.md"