diff --git a/cli/userlist.go b/cli/userlist.go index 108c22cc0fca7..7a9b9cb7908e7 100644 --- a/cli/userlist.go +++ b/cli/userlist.go @@ -2,12 +2,9 @@ package cli import ( "fmt" - "time" - "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" - "github.com/coder/coder/cli/cliui" "github.com/coder/coder/codersdk" ) @@ -28,25 +25,11 @@ func userList() *cobra.Command { return err } - tableWriter := cliui.Table() - header := table.Row{"Username", "Email", "Created At"} - tableWriter.AppendHeader(header) - tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns)) - tableWriter.SortBy([]table.SortBy{{ - Name: "Username", - }}) - for _, user := range users { - tableWriter.AppendRow(table.Row{ - user.Username, - user.Email, - user.CreatedAt.Format(time.Stamp), - }) - } - _, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render()) + _, err = fmt.Fprintln(cmd.OutOrStdout(), displayUsers(columns, users...)) return err }, } - cmd.Flags().StringArrayVarP(&columns, "column", "c", nil, + cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"username", "email", "created_at"}, "Specify a column to filter in the table.") return cmd } diff --git a/cli/users.go b/cli/users.go index dcc03c8ddfa02..29b20b3d1d60e 100644 --- a/cli/users.go +++ b/cli/users.go @@ -1,12 +1,48 @@ package cli -import "github.com/spf13/cobra" +import ( + "time" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/codersdk" +) func users() *cobra.Command { cmd := &cobra.Command{ Short: "Create, remove, and list users", Use: "users", } - cmd.AddCommand(userCreate(), userList()) + cmd.AddCommand( + userCreate(), + userList(), + createUserStatusCommand(codersdk.UserStatusActive), + createUserStatusCommand(codersdk.UserStatusSuspended), + ) return cmd } + +// displayUsers will return a table displaying all users passed in. +// filterColumns must be a subset of the user fields and will determine which +// columns to display +func displayUsers(filterColumns []string, users ...codersdk.User) string { + tableWriter := cliui.Table() + header := table.Row{"id", "username", "email", "created_at", "status"} + tableWriter.AppendHeader(header) + tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, filterColumns)) + tableWriter.SortBy([]table.SortBy{{ + Name: "Username", + }}) + for _, user := range users { + tableWriter.AppendRow(table.Row{ + user.ID.String(), + user.Username, + user.Email, + user.CreatedAt.Format(time.Stamp), + user.Status, + }) + } + return tableWriter.Render() +} diff --git a/cli/userstatus.go b/cli/userstatus.go new file mode 100644 index 0000000000000..152584d8244d5 --- /dev/null +++ b/cli/userstatus.go @@ -0,0 +1,85 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/codersdk" +) + +// createUserStatusCommand sets a user status. +func createUserStatusCommand(sdkStatus codersdk.UserStatus) *cobra.Command { + var verb string + var aliases []string + var short string + switch sdkStatus { + case codersdk.UserStatusActive: + verb = "activate" + aliases = []string{"active"} + short = "Update a user's status to 'active'. Active users can fully interact with the platform" + case codersdk.UserStatusSuspended: + verb = "suspend" + 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)) + } + + var ( + columns []string + ) + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s ", verb), + Short: short, + Args: cobra.ExactArgs(1), + Aliases: aliases, + Example: fmt.Sprintf("coder users %s example_user", verb), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := createClient(cmd) + if err != nil { + return err + } + + identifier := args[0] + if identifier == "" { + return xerrors.Errorf("user identifier cannot be an empty string") + } + + user, err := client.User(cmd.Context(), identifier) + if err != nil { + return xerrors.Errorf("fetch user: %w", err) + } + + // Display the user + _, _ = fmt.Fprintln(cmd.OutOrStdout(), displayUsers(columns, user)) + + // User status is already set to this + if user.Status == sdkStatus { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "User status is already %q\n", sdkStatus) + return nil + } + + // Prompt to confirm the action + _, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: fmt.Sprintf("Are you sure you want to %s this user?", verb), + IsConfirm: true, + Default: "yes", + }) + if err != nil { + return err + } + + _, err = client.UpdateUserStatus(cmd.Context(), user.ID.String(), sdkStatus) + if err != nil { + return xerrors.Errorf("%s user: %w", verb, err) + } + return nil + }, + } + cmd.Flags().StringArrayVarP(&columns, "column", "c", []string{"username", "email", "created_at", "status"}, + "Specify a column to filter in the table.") + return cmd +} diff --git a/cli/userstatus_test.go b/cli/userstatus_test.go new file mode 100644 index 0000000000000..700c08616d84b --- /dev/null +++ b/cli/userstatus_test.go @@ -0,0 +1,64 @@ +package cli_test + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" +) + +func TestUserStatus(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + admin := coderdtest.CreateFirstUser(t, client) + other := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) + otherUser, err := other.User(context.Background(), codersdk.Me) + require.NoError(t, err, "fetch user") + + //nolint:paralleltest + t.Run("StatusSelf", func(t *testing.T) { + cmd, root := clitest.New(t, "users", "suspend", "me") + clitest.SetupConfig(t, client, root) + // Yes to the prompt + cmd.SetIn(bytes.NewReader([]byte("yes\n"))) + err := cmd.Execute() + // Expect an error, as you cannot suspend yourself + require.Error(t, err) + require.ErrorContains(t, err, "cannot suspend yourself") + }) + + //nolint:paralleltest + t.Run("StatusOther", func(t *testing.T) { + require.Equal(t, otherUser.Status, codersdk.UserStatusActive, "start as active") + + cmd, root := clitest.New(t, "users", "suspend", otherUser.Username) + clitest.SetupConfig(t, client, root) + // Yes to the prompt + cmd.SetIn(bytes.NewReader([]byte("yes\n"))) + err := cmd.Execute() + require.NoError(t, err, "suspend user") + + // Check the user status + otherUser, err = client.User(context.Background(), otherUser.Username) + require.NoError(t, err, "fetch suspended user") + require.Equal(t, otherUser.Status, codersdk.UserStatusSuspended, "suspended user") + + // Set back to active. Try using a uuid as well + cmd, root = clitest.New(t, "users", "activate", otherUser.ID.String()) + clitest.SetupConfig(t, client, root) + // Yes to the prompt + cmd.SetIn(bytes.NewReader([]byte("yes\n"))) + err = cmd.Execute() + require.NoError(t, err, "suspend user") + + // Check the user status + otherUser, err = client.User(context.Background(), otherUser.ID.String()) + require.NoError(t, err, "fetch active user") + require.Equal(t, otherUser.Status, codersdk.UserStatusActive, "active user") + }) +} diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index 51f3864e16e38..445be2d8889c3 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -78,7 +78,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) { require.Empty(t, workspace.AutostartSchedule) // Given: the workspace template has been updated - orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID) + orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String()) require.NoError(t, err) require.Len(t, orgs, 1) diff --git a/coderd/coderd.go b/coderd/coderd.go index 4d394cb9362ae..4f717ba3f58f5 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -239,7 +239,10 @@ func New(options *Options) (http.Handler, func()) { r.Use(httpmw.ExtractUserParam(options.Database)) r.Get("/", api.userByName) r.Put("/profile", api.putUserProfile) - r.Put("/suspend", api.putUserSuspend) + r.Route("/status", func(r chi.Router) { + r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended)) + r.Put("/active", api.putUserStatus(database.UserStatusActive)) + }) r.Route("/password", func(r chi.Router) { r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole)) r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate)) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 75f49cea88380..ff15c79c99dae 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -254,7 +254,7 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uui } // TODO: @emyrk switch "other" to "client" when we support updating other // users. - _, err := other.UpdateUserRoles(context.Background(), user.ID, codersdk.UpdateRoles{Roles: siteRoles}) + _, err := other.UpdateUserRoles(context.Background(), user.ID.String(), codersdk.UpdateRoles{Roles: siteRoles}) require.NoError(t, err, "update site roles") // Update org roles @@ -262,7 +262,7 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uui organizationID, err := uuid.Parse(orgID) require.NoError(t, err, fmt.Sprintf("parse org id %q", orgID)) // TODO: @Emyrk add the member to the organization if they do not already belong. - _, err = other.UpdateOrganizationMemberRoles(context.Background(), organizationID, user.ID, + _, err = other.UpdateOrganizationMemberRoles(context.Background(), organizationID, user.ID.String(), codersdk.UpdateRoles{Roles: append(roles, rbac.RoleOrgMember(organizationID))}) require.NoError(t, err, "update org membership roles") } diff --git a/coderd/gitsshkey_test.go b/coderd/gitsshkey_test.go index 1839cd9bd31fb..fe0071c67c262 100644 --- a/coderd/gitsshkey_test.go +++ b/coderd/gitsshkey_test.go @@ -21,7 +21,7 @@ func TestGitSSHKey(t *testing.T) { ctx := context.Background() client := coderdtest.New(t, nil) res := coderdtest.CreateFirstUser(t, client) - key, err := client.GitSSHKey(ctx, res.UserID) + key, err := client.GitSSHKey(ctx, res.UserID.String()) require.NoError(t, err) require.NotEmpty(t, key.PublicKey) }) @@ -32,7 +32,7 @@ func TestGitSSHKey(t *testing.T) { SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519, }) res := coderdtest.CreateFirstUser(t, client) - key, err := client.GitSSHKey(ctx, res.UserID) + key, err := client.GitSSHKey(ctx, res.UserID.String()) require.NoError(t, err) require.NotEmpty(t, key.PublicKey) }) @@ -43,7 +43,7 @@ func TestGitSSHKey(t *testing.T) { SSHKeygenAlgorithm: gitsshkey.AlgorithmECDSA, }) res := coderdtest.CreateFirstUser(t, client) - key, err := client.GitSSHKey(ctx, res.UserID) + key, err := client.GitSSHKey(ctx, res.UserID.String()) require.NoError(t, err) require.NotEmpty(t, key.PublicKey) }) @@ -54,7 +54,7 @@ func TestGitSSHKey(t *testing.T) { SSHKeygenAlgorithm: gitsshkey.AlgorithmRSA4096, }) res := coderdtest.CreateFirstUser(t, client) - key, err := client.GitSSHKey(ctx, res.UserID) + key, err := client.GitSSHKey(ctx, res.UserID.String()) require.NoError(t, err) require.NotEmpty(t, key.PublicKey) }) @@ -65,10 +65,10 @@ func TestGitSSHKey(t *testing.T) { SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519, }) res := coderdtest.CreateFirstUser(t, client) - key1, err := client.GitSSHKey(ctx, res.UserID) + key1, err := client.GitSSHKey(ctx, res.UserID.String()) require.NoError(t, err) require.NotEmpty(t, key1.PublicKey) - key2, err := client.RegenerateGitSSHKey(ctx, res.UserID) + key2, err := client.RegenerateGitSSHKey(ctx, res.UserID.String()) require.NoError(t, err) require.GreaterOrEqual(t, key2.UpdatedAt, key1.UpdatedAt) require.NotEmpty(t, key2.PublicKey) diff --git a/coderd/roles_test.go b/coderd/roles_test.go index 2c2629c461793..83d4f1f23d83a 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -107,7 +107,7 @@ func TestListRoles(t *testing.T) { member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID)) - otherOrg, err := client.CreateOrganization(ctx, admin.UserID, codersdk.CreateOrganizationRequest{ + otherOrg, err := client.CreateOrganization(ctx, admin.UserID.String(), codersdk.CreateOrganizationRequest{ Name: "other", }) require.NoError(t, err, "create org") diff --git a/coderd/users.go b/coderd/users.go index 3900a4d4e7340..5f34b951223f6 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -303,31 +303,40 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs)) } -func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) { - user := httpmw.UserParam(r) - - suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{ - ID: user.ID, - Status: database.UserStatusSuspended, - UpdatedAt: database.Now(), - }) +func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + apiKey := httpmw.APIKey(r) + if status == database.UserStatusSuspended && user.ID == apiKey.UserID { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "You cannot suspend yourself", + }) + return + } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("put user suspended: %s", err.Error()), + suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{ + ID: user.ID, + Status: status, + UpdatedAt: database.Now(), }) - return - } - organizations, err := userOrganizationIDs(r.Context(), api, user) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization IDs: %s", err.Error()), - }) - return - } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("put user suspended: %s", err.Error()), + }) + return + } + + organizations, err := userOrganizationIDs(r.Context(), api, user) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization IDs: %s", err.Error()), + }) + return + } - httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations)) + httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations)) + } } func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/users_test.go b/coderd/users_test.go index 488afe697a7eb..ef4eabe74972e 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -209,7 +209,7 @@ func TestUpdateUserProfile(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) - _, err := client.UpdateUserProfile(context.Background(), uuid.New(), codersdk.UpdateUserProfileRequest{ + _, err := client.UpdateUserProfile(context.Background(), uuid.New().String(), codersdk.UpdateUserProfileRequest{ Username: "newusername", Email: "newemail@coder.com", }) @@ -295,7 +295,7 @@ func TestUpdateUserPassword(t *testing.T) { client := coderdtest.New(t, nil) admin := coderdtest.CreateFirstUser(t, client) member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) - err := member.UpdateUserPassword(context.Background(), admin.UserID, codersdk.UpdateUserPasswordRequest{ + err := member.UpdateUserPassword(context.Background(), admin.UserID.String(), codersdk.UpdateUserPasswordRequest{ Password: "newpassword", }) require.Error(t, err, "member should not be able to update admin password") @@ -312,7 +312,7 @@ func TestUpdateUserPassword(t *testing.T) { OrganizationID: admin.OrganizationID, }) require.NoError(t, err, "create member") - err = client.UpdateUserPassword(context.Background(), member.ID, codersdk.UpdateUserPasswordRequest{ + err = client.UpdateUserPassword(context.Background(), member.ID.String(), codersdk.UpdateUserPasswordRequest{ Password: "newpassword", }) require.NoError(t, err, "admin should be able to update member password") @@ -339,7 +339,7 @@ func TestGrantRoles(t *testing.T) { }) require.Error(t, err, "org role in site") - _, err = admin.UpdateUserRoles(ctx, uuid.New(), codersdk.UpdateRoles{ + _, err = admin.UpdateUserRoles(ctx, uuid.New().String(), codersdk.UpdateRoles{ Roles: []string{rbac.RoleOrgMember(first.OrganizationID)}, }) require.Error(t, err, "user does not exist") @@ -354,12 +354,12 @@ func TestGrantRoles(t *testing.T) { }) require.Error(t, err, "role in org without membership") - _, err = member.UpdateUserRoles(ctx, first.UserID, codersdk.UpdateRoles{ + _, err = member.UpdateUserRoles(ctx, first.UserID.String(), codersdk.UpdateRoles{ Roles: []string{rbac.RoleMember()}, }) require.Error(t, err, "member cannot change other's roles") - _, err = member.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, first.UserID, codersdk.UpdateRoles{ + _, err = member.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, first.UserID.String(), codersdk.UpdateRoles{ Roles: []string{rbac.RoleMember()}, }) require.Error(t, err, "member cannot change other's org roles") @@ -452,7 +452,7 @@ func TestPutUserSuspend(t *testing.T) { Password: "password", OrganizationID: me.OrganizationID, }) - user, err := client.SuspendUser(context.Background(), user.ID) + user, err := client.UpdateUserStatus(context.Background(), user.Username, codersdk.UserStatusSuspended) require.NoError(t, err) require.Equal(t, user.Status, codersdk.UserStatusSuspended) }) @@ -462,10 +462,9 @@ func TestPutUserSuspend(t *testing.T) { client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) client.User(context.Background(), codersdk.Me) - suspendedUser, err := client.SuspendUser(context.Background(), codersdk.Me) + _, err := client.UpdateUserStatus(context.Background(), codersdk.Me, codersdk.UserStatusSuspended) - require.NoError(t, err) - require.Equal(t, suspendedUser.Status, codersdk.UserStatusSuspended) + require.ErrorContains(t, err, "suspend yourself", "cannot suspend yourself") }) } @@ -490,7 +489,7 @@ func TestGetUser(t *testing.T) { client := coderdtest.New(t, nil) firstUser := coderdtest.CreateFirstUser(t, client) - user, err := client.User(context.Background(), firstUser.UserID) + user, err := client.User(context.Background(), firstUser.UserID.String()) require.NoError(t, err) require.Equal(t, firstUser.UserID, user.ID) require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0]) @@ -501,10 +500,10 @@ func TestGetUser(t *testing.T) { client := coderdtest.New(t, nil) firstUser := coderdtest.CreateFirstUser(t, client) - exp, err := client.User(context.Background(), firstUser.UserID) + exp, err := client.User(context.Background(), firstUser.UserID.String()) require.NoError(t, err) - user, err := client.UserByUsername(context.Background(), exp.Username) + user, err := client.User(context.Background(), exp.Username) require.NoError(t, err) require.Equal(t, exp, user) }) @@ -530,9 +529,15 @@ func TestGetUsers(t *testing.T) { }) t.Run("ActiveUsers", func(t *testing.T) { t.Parallel() + active := make([]codersdk.User, 0) client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) - active := make([]codersdk.User, 0) + + firstUser, err := client.User(context.Background(), first.UserID.String()) + require.NoError(t, err, "") + active = append(active, firstUser) + + // Alice will be suspended alice, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{ Email: "alice@email.com", Username: "alice", @@ -540,7 +545,6 @@ func TestGetUsers(t *testing.T) { OrganizationID: first.OrganizationID, }) require.NoError(t, err) - active = append(active, alice) bruno, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{ Email: "bruno@email.com", @@ -551,7 +555,7 @@ func TestGetUsers(t *testing.T) { require.NoError(t, err) active = append(active, bruno) - _, err = client.SuspendUser(context.Background(), first.UserID) + _, err = client.UpdateUserStatus(context.Background(), alice.Username, codersdk.UserStatusSuspended) require.NoError(t, err) users, err := client.Users(context.Background(), codersdk.UsersRequest{ diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 8ba7a7a232424..3e4d8b57244c5 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -297,7 +297,7 @@ func TestPostWorkspaceBuild(t *testing.T) { require.Equal(t, workspace.LatestBuild.ID.String(), build.BeforeID.String()) coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) - workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, user.UserID) + workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, user.UserID.String()) require.NoError(t, err) require.Len(t, workspaces, 0) }) diff --git a/codersdk/gitsshkey.go b/codersdk/gitsshkey.go index 3cc4333e735e3..f20c0666caf0f 100644 --- a/codersdk/gitsshkey.go +++ b/codersdk/gitsshkey.go @@ -24,8 +24,8 @@ type AgentGitSSHKey struct { } // GitSSHKey returns the user's git SSH public key. -func (c *Client) GitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", uuidOrMe(userID)), nil) +func (c *Client) GitSSHKey(ctx context.Context, user string) (GitSSHKey, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", user), nil) if err != nil { return GitSSHKey{}, xerrors.Errorf("execute request: %w", err) } @@ -40,8 +40,8 @@ func (c *Client) GitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, er } // RegenerateGitSSHKey will create a new SSH key pair for the user and return it. -func (c *Client) RegenerateGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/gitsshkey", uuidOrMe(userID)), nil) +func (c *Client) RegenerateGitSSHKey(ctx context.Context, user string) (GitSSHKey, error) { + res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/gitsshkey", user), nil) if err != nil { return GitSSHKey{}, xerrors.Errorf("execute request: %w", err) } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index ca85dfbbc139e..9e16e16631f98 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -201,8 +201,8 @@ func (c *Client) WorkspacesByOrganization(ctx context.Context, organizationID uu } // WorkspacesByOwner returns all workspaces contained in the organization owned by the user. -func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID, userID uuid.UUID) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, uuidOrMe(userID)), nil) +func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID uuid.UUID, user string) ([]Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, user), nil) if err != nil { return nil, err } @@ -217,8 +217,8 @@ func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID, userID u } // WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name. -func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, organization, owner uuid.UUID, name string) (Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, uuidOrMe(owner), name), nil) +func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, organization uuid.UUID, owner string, name string) (Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, owner, name), nil) if err != nil { return Workspace{}, err } diff --git a/codersdk/roles.go b/codersdk/roles.go index 59b52330b5fb1..09aa19b806ccd 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -45,7 +45,7 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Ro } func (c *Client) CheckPermissions(ctx context.Context, checks UserAuthorizationRequest) (UserAuthorizationResponse, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/authorization", uuidOrMe(Me)), checks) + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/authorization", Me), checks) if err != nil { return nil, err } diff --git a/codersdk/users.go b/codersdk/users.go index 3946d485799dc..8af79d720fff0 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -8,10 +8,11 @@ import ( "time" "github.com/google/uuid" + "golang.org/x/xerrors" ) // Me is used as a replacement for your own ID. -var Me = uuid.Nil +var Me = "me" type UserStatus string @@ -198,8 +199,8 @@ func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, e } // UpdateUserProfile enables callers to update profile information -func (c *Client) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req UpdateUserProfileRequest) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", uuidOrMe(userID)), req) +func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateUserProfileRequest) (User, error) { + res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", user), req) if err != nil { return User{}, err } @@ -207,13 +208,23 @@ func (c *Client) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req Up if res.StatusCode != http.StatusOK { return User{}, readBodyAsError(res) } - var user User - return user, json.NewDecoder(res.Body).Decode(&user) + var resp User + return resp, json.NewDecoder(res.Body).Decode(&resp) } -// SuspendUser enables callers to suspend a user -func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/suspend", uuidOrMe(userID)), nil) +// UpdateUserStatus sets the user status to the given status +func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserStatus) (User, error) { + path := fmt.Sprintf("/api/v2/users/%s/status/", user) + switch status { + case UserStatusActive: + path += "active" + case UserStatusSuspended: + path += "suspend" + default: + return User{}, xerrors.Errorf("status %q is not supported", status) + } + + res, err := c.request(ctx, http.MethodPut, path, nil) if err != nil { return User{}, err } @@ -222,14 +233,14 @@ func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error return User{}, readBodyAsError(res) } - var user User - return user, json.NewDecoder(res.Body).Decode(&user) + var resp User + return resp, json.NewDecoder(res.Body).Decode(&resp) } // UpdateUserPassword updates a user password. // It calls PUT /users/{user}/password -func (c *Client) UpdateUserPassword(ctx context.Context, userID uuid.UUID, req UpdateUserPasswordRequest) error { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", uuidOrMe(userID)), req) +func (c *Client) UpdateUserPassword(ctx context.Context, user string, req UpdateUserPasswordRequest) error { + res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", user), req) if err != nil { return err } @@ -242,8 +253,8 @@ func (c *Client) UpdateUserPassword(ctx context.Context, userID uuid.UUID, req U // UpdateUserRoles grants the userID the specified roles. // Include ALL roles the user has. -func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req UpdateRoles) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), req) +func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRoles) (User, error) { + res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", user), req) if err != nil { return User{}, err } @@ -251,14 +262,14 @@ func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req Upda if res.StatusCode != http.StatusOK { return User{}, readBodyAsError(res) } - var user User - return user, json.NewDecoder(res.Body).Decode(&user) + var resp User + return resp, json.NewDecoder(res.Body).Decode(&resp) } // UpdateOrganizationMemberRoles grants the userID the specified roles in an org. // Include ALL roles the user has. -func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organizationID, userID uuid.UUID, req UpdateRoles) (OrganizationMember, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, uuidOrMe(userID)), req) +func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organizationID uuid.UUID, user string, req UpdateRoles) (OrganizationMember, error) { + res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, user), req) if err != nil { return OrganizationMember{}, err } @@ -271,8 +282,8 @@ func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organization } // GetUserRoles returns all roles the user has -func (c *Client) GetUserRoles(ctx context.Context, userID uuid.UUID) (UserRoles, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), nil) +func (c *Client) GetUserRoles(ctx context.Context, user string) (UserRoles, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", user), nil) if err != nil { return UserRoles{}, err } @@ -285,8 +296,8 @@ func (c *Client) GetUserRoles(ctx context.Context, userID uuid.UUID) (UserRoles, } // CreateAPIKey generates an API key for the user ID provided. -func (c *Client) CreateAPIKey(ctx context.Context, userID uuid.UUID) (*GenerateAPIKeyResponse, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", uuidOrMe(userID)), nil) +func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKeyResponse, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil) if err != nil { return nil, err } @@ -330,24 +341,14 @@ func (c *Client) Logout(ctx context.Context) error { return nil } -// User returns a user for the ID provided. -// If the uuid is nil, the current user will be returned. -func (c *Client) User(ctx context.Context, id uuid.UUID) (User, error) { - return c.userByIdentifier(ctx, uuidOrMe(id)) -} - -// UserByUsername returns a user for the username provided. -func (c *Client) UserByUsername(ctx context.Context, username string) (User, error) { - return c.userByIdentifier(ctx, username) -} - -func (c *Client) userByIdentifier(ctx context.Context, ident string) (User, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", ident), nil) +// User returns a user for the ID/username provided. +func (c *Client) User(ctx context.Context, userIdent string) (User, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", userIdent), nil) if err != nil { return User{}, err } defer res.Body.Close() - if res.StatusCode > http.StatusOK { + if res.StatusCode != http.StatusOK { return User{}, readBodyAsError(res) } var user User @@ -380,8 +381,8 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) { } // OrganizationsByUser returns all organizations the user is a member of. -func (c *Client) OrganizationsByUser(ctx context.Context, userID uuid.UUID) ([]Organization, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", uuidOrMe(userID)), nil) +func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organization, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", user), nil) if err != nil { return nil, err } @@ -393,8 +394,8 @@ func (c *Client) OrganizationsByUser(ctx context.Context, userID uuid.UUID) ([]O return orgs, json.NewDecoder(res.Body).Decode(&orgs) } -func (c *Client) OrganizationByName(ctx context.Context, userID uuid.UUID, name string) (Organization, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", uuidOrMe(userID), name), nil) +func (c *Client) OrganizationByName(ctx context.Context, user string, name string) (Organization, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil) if err != nil { return Organization{}, err } @@ -407,8 +408,8 @@ func (c *Client) OrganizationByName(ctx context.Context, userID uuid.UUID, name } // CreateOrganization creates an organization and adds the provided user as an admin. -func (c *Client) CreateOrganization(ctx context.Context, userID uuid.UUID, req CreateOrganizationRequest) (Organization, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", uuidOrMe(userID)), req) +func (c *Client) CreateOrganization(ctx context.Context, user string, req CreateOrganizationRequest) (Organization, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", user), req) if err != nil { return Organization{}, err } @@ -439,8 +440,8 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { } // WorkspacesByUser returns all workspaces a user has access to. -func (c *Client) WorkspacesByUser(ctx context.Context, userID uuid.UUID) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", uuidOrMe(userID)), nil) +func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", user), nil) if err != nil { return nil, err } @@ -453,13 +454,3 @@ func (c *Client) WorkspacesByUser(ctx context.Context, userID uuid.UUID) ([]Work var workspaces []Workspace return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) } - -// uuidOrMe returns the provided uuid as a string if it's valid, ortherwise -// `me`. -func uuidOrMe(id uuid.UUID) string { - if id == Me { - return "me" - } - - return id.String() -}