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
This commit is contained in:
Steven Masley 2022-05-16 15:29:27 -05:00 committed by GitHub
parent a77da8445e
commit b55d83ca82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 308 additions and 133 deletions

View File

@ -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
}

View File

@ -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()
}

85
cli/userstatus.go Normal file
View File

@ -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 <username|user_id>", 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
}

64
cli/userstatus_test.go Normal file
View File

@ -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")
})
}

View File

@ -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)

View File

@ -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))

View File

@ -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")
}

View File

@ -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)

View File

@ -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")

View File

@ -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)
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
}
suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusSuspended,
UpdatedAt: database.Now(),
})
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
}
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
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))
}
}
func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {

View File

@ -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{

View File

@ -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)
})

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}