feat: add edit-role within user command (#17341)

This commit is contained in:
brettkolodny
2025-04-15 18:30:20 -04:00
committed by GitHub
parent 57ddb3c615
commit 70b113de7b
8 changed files with 223 additions and 17 deletions
+1
View File
@@ -12,6 +12,7 @@ SUBCOMMANDS:
interact with the platform
create
delete Delete a user by username or user_id.
edit-roles Edit a user's roles by username or id
list
show Show a single user. Use 'me' to indicate the currently
authenticated user.
+18
View File
@@ -0,0 +1,18 @@
coder v0.0.0-devel
USAGE:
coder users edit-roles [flags] <username|user_id>
Edit a user's roles by username or id
OPTIONS:
--roles string-array
A list of roles to give to the user. This removes any existing roles
the user may have. The available roles are: auditor, member, owner,
template-admin, user-admin.
-y, --yes bool
Bypass prompts.
———
Run `coder --help` for a list of global options.
+90
View File
@@ -0,0 +1,90 @@
package cli
import (
"fmt"
"slices"
"sort"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) userEditRoles() *serpent.Command {
client := new(codersdk.Client)
roles := rbac.SiteRoles()
siteRoles := make([]string, 0)
for _, role := range roles {
siteRoles = append(siteRoles, role.Identifier.Name)
}
sort.Strings(siteRoles)
var givenRoles []string
cmd := &serpent.Command{
Use: "edit-roles <username|user_id>",
Short: "Edit a user's roles by username or id",
Options: []serpent.Option{
cliui.SkipPromptOption(),
{
Name: "roles",
Description: fmt.Sprintf("A list of roles to give to the user. This removes any existing roles the user may have. The available roles are: %s.", strings.Join(siteRoles, ", ")),
Flag: "roles",
Value: serpent.StringArrayOf(&givenRoles),
},
},
Middleware: serpent.Chain(serpent.RequireNArgs(1), r.InitClient(client)),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
user, err := client.User(ctx, inv.Args[0])
if err != nil {
return xerrors.Errorf("fetch user: %w", err)
}
userRoles, err := client.UserRoles(ctx, user.Username)
if err != nil {
return xerrors.Errorf("fetch user roles: %w", err)
}
var selectedRoles []string
if len(givenRoles) > 0 {
// Make sure all of the given roles are valid site roles
for _, givenRole := range givenRoles {
if !slices.Contains(siteRoles, givenRole) {
siteRolesPretty := strings.Join(siteRoles, ", ")
return xerrors.Errorf("The role %s is not valid. Please use one or more of the following roles: %s\n", givenRole, siteRolesPretty)
}
}
selectedRoles = givenRoles
} else {
selectedRoles, err = cliui.MultiSelect(inv, cliui.MultiSelectOptions{
Message: "Select the roles you'd like to assign to the user",
Options: siteRoles,
Defaults: userRoles.Roles,
})
if err != nil {
return xerrors.Errorf("selecting roles for user: %w", err)
}
}
_, err = client.UpdateUserRoles(ctx, user.Username, codersdk.UpdateRoles{
Roles: selectedRoles,
})
if err != nil {
return xerrors.Errorf("update user roles: %w", err)
}
return nil
},
}
return cmd
}
+62
View File
@@ -0,0 +1,62 @@
package cli_test
import (
"fmt"
"strings"
"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/coderd/rbac"
"github.com/coder/coder/v2/testutil"
)
var roles = []string{"auditor", "user-admin"}
func TestUserEditRoles(t *testing.T) {
t.Parallel()
t.Run("UpdateUserRoles", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
userAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleOwner())
_, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
inv, root := clitest.New(t, "users", "edit-roles", member.Username, fmt.Sprintf("--roles=%s", strings.Join(roles, ",")))
clitest.SetupConfig(t, userAdmin, root)
// Create context with timeout
ctx := testutil.Context(t, testutil.WaitShort)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
memberRoles, err := client.UserRoles(ctx, member.Username)
require.NoError(t, err)
require.ElementsMatch(t, memberRoles.Roles, roles)
})
t.Run("UserNotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
userAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleUserAdmin())
// Setup command with non-existent user
inv, root := clitest.New(t, "users", "edit-roles", "nonexistentuser")
clitest.SetupConfig(t, userAdmin, root)
// Create context with timeout
ctx := testutil.Context(t, testutil.WaitShort)
err := inv.WithContext(ctx).Run()
require.Error(t, err)
require.Contains(t, err.Error(), "fetch user")
})
}
+1
View File
@@ -18,6 +18,7 @@ func (r *RootCmd) users() *serpent.Command {
r.userList(),
r.userSingle(),
r.userDelete(),
r.userEditRoles(),
r.createUserStatusCommand(codersdk.UserStatusActive),
r.createUserStatusCommand(codersdk.UserStatusSuspended),
},
+5
View File
@@ -1605,6 +1605,11 @@
"description": "Delete a user by username or user_id.",
"path": "reference/cli/users_delete.md"
},
{
"title": "users edit-roles",
"description": "Edit a user's roles by username or id",
"path": "reference/cli/users_edit-roles.md"
},
{
"title": "users list",
"path": "reference/cli/users_list.md"
+2 -1
View File
@@ -16,10 +16,11 @@ coder users [subcommand]
## Subcommands
| Name | Purpose |
|----------------------------------------------|---------------------------------------------------------------------------------------|
|--------------------------------------------------|---------------------------------------------------------------------------------------|
| [<code>create</code>](./users_create.md) | |
| [<code>list</code>](./users_list.md) | |
| [<code>show</code>](./users_show.md) | Show a single user. Use 'me' to indicate the currently authenticated user. |
| [<code>delete</code>](./users_delete.md) | Delete a user by username or user_id. |
| [<code>edit-roles</code>](./users_edit-roles.md) | Edit a user's roles by username or id |
| [<code>activate</code>](./users_activate.md) | Update a user's status to 'active'. Active users can fully interact with the platform |
| [<code>suspend</code>](./users_suspend.md) | Update a user's status to 'suspended'. A suspended user cannot log into the platform |
+28
View File
@@ -0,0 +1,28 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# users edit-roles
Edit a user's roles by username or id
## Usage
```console
coder users edit-roles [flags] <username|user_id>
```
## Options
### -y, --yes
| | |
|------|-------------------|
| Type | <code>bool</code> |
Bypass prompts.
### --roles
| | |
|------|---------------------------|
| Type | <code>string-array</code> |
A list of roles to give to the user. This removes any existing roles the user may have. The available roles are: auditor, member, owner, template-admin, user-admin.