Skip to content

Commit b76740a

Browse files
Emyrkkylecarbs
authored andcommitted
feat: Add suspend/active user to cli (#1422)
* feat: Add suspend/active user to cli * UserID is now a string and allows for username too
1 parent f4c18cf commit b76740a

16 files changed

+308
-133
lines changed

cli/userlist.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ package cli
22

33
import (
44
"fmt"
5-
"time"
65

7-
"github.com/jedib0t/go-pretty/v6/table"
86
"github.com/spf13/cobra"
97

10-
"github.com/coder/coder/cli/cliui"
118
"github.com/coder/coder/codersdk"
129
)
1310

@@ -28,25 +25,11 @@ func userList() *cobra.Command {
2825
return err
2926
}
3027

31-
tableWriter := cliui.Table()
32-
header := table.Row{"Username", "Email", "Created At"}
33-
tableWriter.AppendHeader(header)
34-
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))
35-
tableWriter.SortBy([]table.SortBy{{
36-
Name: "Username",
37-
}})
38-
for _, user := range users {
39-
tableWriter.AppendRow(table.Row{
40-
user.Username,
41-
user.Email,
42-
user.CreatedAt.Format(time.Stamp),
43-
})
44-
}
45-
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
28+
_, err = fmt.Fprintln(cmd.OutOrStdout(), displayUsers(columns, users...))
4629
return err
4730
},
4831
}
49-
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
32+
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"username", "email", "created_at"},
5033
"Specify a column to filter in the table.")
5134
return cmd
5235
}

cli/users.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
11
package cli
22

3-
import "github.com/spf13/cobra"
3+
import (
4+
"time"
5+
6+
"github.com/jedib0t/go-pretty/v6/table"
7+
"github.com/spf13/cobra"
8+
9+
"github.com/coder/coder/cli/cliui"
10+
"github.com/coder/coder/codersdk"
11+
)
412

513
func users() *cobra.Command {
614
cmd := &cobra.Command{
715
Short: "Create, remove, and list users",
816
Use: "users",
917
}
10-
cmd.AddCommand(userCreate(), userList())
18+
cmd.AddCommand(
19+
userCreate(),
20+
userList(),
21+
createUserStatusCommand(codersdk.UserStatusActive),
22+
createUserStatusCommand(codersdk.UserStatusSuspended),
23+
)
1124
return cmd
1225
}
26+
27+
// displayUsers will return a table displaying all users passed in.
28+
// filterColumns must be a subset of the user fields and will determine which
29+
// columns to display
30+
func displayUsers(filterColumns []string, users ...codersdk.User) string {
31+
tableWriter := cliui.Table()
32+
header := table.Row{"id", "username", "email", "created_at", "status"}
33+
tableWriter.AppendHeader(header)
34+
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, filterColumns))
35+
tableWriter.SortBy([]table.SortBy{{
36+
Name: "Username",
37+
}})
38+
for _, user := range users {
39+
tableWriter.AppendRow(table.Row{
40+
user.ID.String(),
41+
user.Username,
42+
user.Email,
43+
user.CreatedAt.Format(time.Stamp),
44+
user.Status,
45+
})
46+
}
47+
return tableWriter.Render()
48+
}

cli/userstatus.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/cli/cliui"
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
// createUserStatusCommand sets a user status.
14+
func createUserStatusCommand(sdkStatus codersdk.UserStatus) *cobra.Command {
15+
var verb string
16+
var aliases []string
17+
var short string
18+
switch sdkStatus {
19+
case codersdk.UserStatusActive:
20+
verb = "activate"
21+
aliases = []string{"active"}
22+
short = "Update a user's status to 'active'. Active users can fully interact with the platform"
23+
case codersdk.UserStatusSuspended:
24+
verb = "suspend"
25+
aliases = []string{"rm", "delete"}
26+
short = "Update a user's status to 'suspended'. A suspended user cannot log into the platform"
27+
default:
28+
panic(fmt.Sprintf("%s is not supported", sdkStatus))
29+
}
30+
31+
var (
32+
columns []string
33+
)
34+
cmd := &cobra.Command{
35+
Use: fmt.Sprintf("%s <username|user_id>", verb),
36+
Short: short,
37+
Args: cobra.ExactArgs(1),
38+
Aliases: aliases,
39+
Example: fmt.Sprintf("coder users %s example_user", verb),
40+
RunE: func(cmd *cobra.Command, args []string) error {
41+
client, err := createClient(cmd)
42+
if err != nil {
43+
return err
44+
}
45+
46+
identifier := args[0]
47+
if identifier == "" {
48+
return xerrors.Errorf("user identifier cannot be an empty string")
49+
}
50+
51+
user, err := client.User(cmd.Context(), identifier)
52+
if err != nil {
53+
return xerrors.Errorf("fetch user: %w", err)
54+
}
55+
56+
// Display the user
57+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), displayUsers(columns, user))
58+
59+
// User status is already set to this
60+
if user.Status == sdkStatus {
61+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "User status is already %q\n", sdkStatus)
62+
return nil
63+
}
64+
65+
// Prompt to confirm the action
66+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
67+
Text: fmt.Sprintf("Are you sure you want to %s this user?", verb),
68+
IsConfirm: true,
69+
Default: "yes",
70+
})
71+
if err != nil {
72+
return err
73+
}
74+
75+
_, err = client.UpdateUserStatus(cmd.Context(), user.ID.String(), sdkStatus)
76+
if err != nil {
77+
return xerrors.Errorf("%s user: %w", verb, err)
78+
}
79+
return nil
80+
},
81+
}
82+
cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"username", "email", "created_at", "status"},
83+
"Specify a column to filter in the table.")
84+
return cmd
85+
}

cli/userstatus_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/cli/clitest"
11+
"github.com/coder/coder/coderd/coderdtest"
12+
"github.com/coder/coder/codersdk"
13+
)
14+
15+
func TestUserStatus(t *testing.T) {
16+
t.Parallel()
17+
client := coderdtest.New(t, nil)
18+
admin := coderdtest.CreateFirstUser(t, client)
19+
other := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
20+
otherUser, err := other.User(context.Background(), codersdk.Me)
21+
require.NoError(t, err, "fetch user")
22+
23+
//nolint:paralleltest
24+
t.Run("StatusSelf", func(t *testing.T) {
25+
cmd, root := clitest.New(t, "users", "suspend", "me")
26+
clitest.SetupConfig(t, client, root)
27+
// Yes to the prompt
28+
cmd.SetIn(bytes.NewReader([]byte("yes\n")))
29+
err := cmd.Execute()
30+
// Expect an error, as you cannot suspend yourself
31+
require.Error(t, err)
32+
require.ErrorContains(t, err, "cannot suspend yourself")
33+
})
34+
35+
//nolint:paralleltest
36+
t.Run("StatusOther", func(t *testing.T) {
37+
require.Equal(t, otherUser.Status, codersdk.UserStatusActive, "start as active")
38+
39+
cmd, root := clitest.New(t, "users", "suspend", otherUser.Username)
40+
clitest.SetupConfig(t, client, root)
41+
// Yes to the prompt
42+
cmd.SetIn(bytes.NewReader([]byte("yes\n")))
43+
err := cmd.Execute()
44+
require.NoError(t, err, "suspend user")
45+
46+
// Check the user status
47+
otherUser, err = client.User(context.Background(), otherUser.Username)
48+
require.NoError(t, err, "fetch suspended user")
49+
require.Equal(t, otherUser.Status, codersdk.UserStatusSuspended, "suspended user")
50+
51+
// Set back to active. Try using a uuid as well
52+
cmd, root = clitest.New(t, "users", "activate", otherUser.ID.String())
53+
clitest.SetupConfig(t, client, root)
54+
// Yes to the prompt
55+
cmd.SetIn(bytes.NewReader([]byte("yes\n")))
56+
err = cmd.Execute()
57+
require.NoError(t, err, "suspend user")
58+
59+
// Check the user status
60+
otherUser, err = client.User(context.Background(), otherUser.ID.String())
61+
require.NoError(t, err, "fetch active user")
62+
require.Equal(t, otherUser.Status, codersdk.UserStatusActive, "active user")
63+
})
64+
}

coderd/autobuild/executor/lifecycle_executor_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
7878
require.Empty(t, workspace.AutostartSchedule)
7979

8080
// Given: the workspace template has been updated
81-
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID)
81+
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
8282
require.NoError(t, err)
8383
require.Len(t, orgs, 1)
8484

coderd/coderd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@ func New(options *Options) (http.Handler, func()) {
239239
r.Use(httpmw.ExtractUserParam(options.Database))
240240
r.Get("/", api.userByName)
241241
r.Put("/profile", api.putUserProfile)
242-
r.Put("/suspend", api.putUserSuspend)
242+
r.Route("/status", func(r chi.Router) {
243+
r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended))
244+
r.Put("/active", api.putUserStatus(database.UserStatusActive))
245+
})
243246
r.Route("/password", func(r chi.Router) {
244247
r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole))
245248
r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate))

coderd/coderdtest/coderdtest.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,15 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uui
254254
}
255255
// TODO: @emyrk switch "other" to "client" when we support updating other
256256
// users.
257-
_, err := other.UpdateUserRoles(context.Background(), user.ID, codersdk.UpdateRoles{Roles: siteRoles})
257+
_, err := other.UpdateUserRoles(context.Background(), user.ID.String(), codersdk.UpdateRoles{Roles: siteRoles})
258258
require.NoError(t, err, "update site roles")
259259

260260
// Update org roles
261261
for orgID, roles := range orgRoles {
262262
organizationID, err := uuid.Parse(orgID)
263263
require.NoError(t, err, fmt.Sprintf("parse org id %q", orgID))
264264
// TODO: @Emyrk add the member to the organization if they do not already belong.
265-
_, err = other.UpdateOrganizationMemberRoles(context.Background(), organizationID, user.ID,
265+
_, err = other.UpdateOrganizationMemberRoles(context.Background(), organizationID, user.ID.String(),
266266
codersdk.UpdateRoles{Roles: append(roles, rbac.RoleOrgMember(organizationID))})
267267
require.NoError(t, err, "update org membership roles")
268268
}

coderd/gitsshkey_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestGitSSHKey(t *testing.T) {
2121
ctx := context.Background()
2222
client := coderdtest.New(t, nil)
2323
res := coderdtest.CreateFirstUser(t, client)
24-
key, err := client.GitSSHKey(ctx, res.UserID)
24+
key, err := client.GitSSHKey(ctx, res.UserID.String())
2525
require.NoError(t, err)
2626
require.NotEmpty(t, key.PublicKey)
2727
})
@@ -32,7 +32,7 @@ func TestGitSSHKey(t *testing.T) {
3232
SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519,
3333
})
3434
res := coderdtest.CreateFirstUser(t, client)
35-
key, err := client.GitSSHKey(ctx, res.UserID)
35+
key, err := client.GitSSHKey(ctx, res.UserID.String())
3636
require.NoError(t, err)
3737
require.NotEmpty(t, key.PublicKey)
3838
})
@@ -43,7 +43,7 @@ func TestGitSSHKey(t *testing.T) {
4343
SSHKeygenAlgorithm: gitsshkey.AlgorithmECDSA,
4444
})
4545
res := coderdtest.CreateFirstUser(t, client)
46-
key, err := client.GitSSHKey(ctx, res.UserID)
46+
key, err := client.GitSSHKey(ctx, res.UserID.String())
4747
require.NoError(t, err)
4848
require.NotEmpty(t, key.PublicKey)
4949
})
@@ -54,7 +54,7 @@ func TestGitSSHKey(t *testing.T) {
5454
SSHKeygenAlgorithm: gitsshkey.AlgorithmRSA4096,
5555
})
5656
res := coderdtest.CreateFirstUser(t, client)
57-
key, err := client.GitSSHKey(ctx, res.UserID)
57+
key, err := client.GitSSHKey(ctx, res.UserID.String())
5858
require.NoError(t, err)
5959
require.NotEmpty(t, key.PublicKey)
6060
})
@@ -65,10 +65,10 @@ func TestGitSSHKey(t *testing.T) {
6565
SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519,
6666
})
6767
res := coderdtest.CreateFirstUser(t, client)
68-
key1, err := client.GitSSHKey(ctx, res.UserID)
68+
key1, err := client.GitSSHKey(ctx, res.UserID.String())
6969
require.NoError(t, err)
7070
require.NotEmpty(t, key1.PublicKey)
71-
key2, err := client.RegenerateGitSSHKey(ctx, res.UserID)
71+
key2, err := client.RegenerateGitSSHKey(ctx, res.UserID.String())
7272
require.NoError(t, err)
7373
require.GreaterOrEqual(t, key2.UpdatedAt, key1.UpdatedAt)
7474
require.NotEmpty(t, key2.PublicKey)

coderd/roles_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func TestListRoles(t *testing.T) {
107107
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
108108
orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID))
109109

110-
otherOrg, err := client.CreateOrganization(ctx, admin.UserID, codersdk.CreateOrganizationRequest{
110+
otherOrg, err := client.CreateOrganization(ctx, admin.UserID.String(), codersdk.CreateOrganizationRequest{
111111
Name: "other",
112112
})
113113
require.NoError(t, err, "create org")

0 commit comments

Comments
 (0)