From d11c17bcc027172673d6be08317846c0d4921caa Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 10 Apr 2025 14:41:56 +0000 Subject: [PATCH 01/14] feat: add an alias for org edit-roles to the users cli path --- cli/users.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cli/users.go b/cli/users.go index 3e6173880c0a3..d24c625085d82 100644 --- a/cli/users.go +++ b/cli/users.go @@ -18,9 +18,18 @@ func (r *RootCmd) users() *serpent.Command { r.userList(), r.userSingle(), r.userDelete(), + r.userEditRoles(), r.createUserStatusCommand(codersdk.UserStatusActive), r.createUserStatusCommand(codersdk.UserStatusSuspended), }, } return cmd } + +func (r *RootCmd) userEditRoles() *serpent.Command { + orgContext := NewOrganizationContext() + cmd := r.assignOrganizationRoles(orgContext) + cmd.Short = "Edit a member's roles" + + return cmd +} From d6a1dc222ddb8c76b690d5aa075684f53ff53490 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 10 Apr 2025 14:54:45 +0000 Subject: [PATCH 02/14] chore: comment code --- cli/users.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/users.go b/cli/users.go index d24c625085d82..04876921ae5c7 100644 --- a/cli/users.go +++ b/cli/users.go @@ -26,6 +26,8 @@ func (r *RootCmd) users() *serpent.Command { return cmd } +// An alias for `organization members edit-roles` for single-organization +// deployments. func (r *RootCmd) userEditRoles() *serpent.Command { orgContext := NewOrganizationContext() cmd := r.assignOrganizationRoles(orgContext) From ff0603d7218aa541b0639e9cbdcc429f4dd6417d Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 10 Apr 2025 14:58:43 +0000 Subject: [PATCH 03/14] fix: make gen --- cli/testdata/coder_users_--help.golden | 19 ++++++++++--------- .../coder_users_edit-roles_--help.golden | 11 +++++++++++ docs/manifest.json | 5 +++++ docs/reference/cli/users.md | 17 +++++++++-------- docs/reference/cli/users_edit-roles.md | 14 ++++++++++++++ .../terraform/testdata/resources/version.txt | 2 +- 6 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 cli/testdata/coder_users_edit-roles_--help.golden create mode 100644 docs/reference/cli/users_edit-roles.md diff --git a/cli/testdata/coder_users_--help.golden b/cli/testdata/coder_users_--help.golden index 338fea4febc86..9f178ec281c00 100644 --- a/cli/testdata/coder_users_--help.golden +++ b/cli/testdata/coder_users_--help.golden @@ -8,15 +8,16 @@ USAGE: Aliases: user 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. - suspend Update a user's status to 'suspended'. A suspended user cannot - log into the platform + 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. + edit-roles Edit a member's roles + list + show Show a single user. Use 'me' to indicate the currently + authenticated user. + suspend Update a user's status to 'suspended'. A suspended user cannot + log into the platform ——— Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_users_edit-roles_--help.golden b/cli/testdata/coder_users_edit-roles_--help.golden new file mode 100644 index 0000000000000..d6adc95b5a400 --- /dev/null +++ b/cli/testdata/coder_users_edit-roles_--help.golden @@ -0,0 +1,11 @@ +coder v0.0.0-devel + +USAGE: + coder users edit-roles [roles...] + + Edit a member's roles + + Aliases: edit-role + +——— +Run `coder --help` for a list of global options. diff --git a/docs/manifest.json b/docs/manifest.json index df535a1687807..c8e051182946a 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1592,6 +1592,11 @@ "description": "Delete a user by username or user_id.", "path": "reference/cli/users_delete.md" }, + { + "title": "users edit-roles", + "description": "Edit a member's roles", + "path": "reference/cli/users_edit-roles.md" + }, { "title": "users list", "path": "reference/cli/users_list.md" diff --git a/docs/reference/cli/users.md b/docs/reference/cli/users.md index 174e08fe9f3a0..d4d970ca1ada9 100644 --- a/docs/reference/cli/users.md +++ b/docs/reference/cli/users.md @@ -15,11 +15,12 @@ coder users [subcommand] ## Subcommands -| Name | Purpose | -|----------------------------------------------|---------------------------------------------------------------------------------------| -| [create](./users_create.md) | | -| [list](./users_list.md) | | -| [show](./users_show.md) | Show a single user. Use 'me' to indicate the currently authenticated user. | -| [delete](./users_delete.md) | Delete a user by username or user_id. | -| [activate](./users_activate.md) | Update a user's status to 'active'. Active users can fully interact with the platform | -| [suspend](./users_suspend.md) | Update a user's status to 'suspended'. A suspended user cannot log into the platform | +| Name | Purpose | +|--------------------------------------------------|---------------------------------------------------------------------------------------| +| [create](./users_create.md) | | +| [list](./users_list.md) | | +| [show](./users_show.md) | Show a single user. Use 'me' to indicate the currently authenticated user. | +| [delete](./users_delete.md) | Delete a user by username or user_id. | +| [edit-roles](./users_edit-roles.md) | Edit a member's roles | +| [activate](./users_activate.md) | Update a user's status to 'active'. Active users can fully interact with the platform | +| [suspend](./users_suspend.md) | Update a user's status to 'suspended'. A suspended user cannot log into the platform | diff --git a/docs/reference/cli/users_edit-roles.md b/docs/reference/cli/users_edit-roles.md new file mode 100644 index 0000000000000..cc08f01fb2fb5 --- /dev/null +++ b/docs/reference/cli/users_edit-roles.md @@ -0,0 +1,14 @@ + +# users edit-roles + +Edit a member's roles + +Aliases: + +* edit-role + +## Usage + +```console +coder users edit-roles [roles...] +``` diff --git a/provisioner/terraform/testdata/resources/version.txt b/provisioner/terraform/testdata/resources/version.txt index 0a5af26df3fdb..3d0e62313ced1 100644 --- a/provisioner/terraform/testdata/resources/version.txt +++ b/provisioner/terraform/testdata/resources/version.txt @@ -1 +1 @@ -1.11.3 +1.11.4 From 85ef42ee4da858761c82c97eb0c1ed7292a8ff77 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 11 Apr 2025 17:22:54 +0000 Subject: [PATCH 04/14] feat: create a command for updating user site-wide roles --- cli/usereditroles.go | 63 ++++++++++++++++++++++++++++++++++++++++++++ cli/userroles.go | 62 +++++++++++++++++++++++++++++++++++++++++++ cli/users.go | 10 ------- 3 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 cli/usereditroles.go create mode 100644 cli/userroles.go diff --git a/cli/usereditroles.go b/cli/usereditroles.go new file mode 100644 index 0000000000000..4fe46b2497d8b --- /dev/null +++ b/cli/usereditroles.go @@ -0,0 +1,63 @@ +package cli + +import ( + // "fmt" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" + "golang.org/x/xerrors" +) + +func (r *RootCmd) userEditRoles() *serpent.Command { + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "edit-roles ", + Short: "Edit a user's roles by username or id", + 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) + } + + roles, err := client.ListSiteRoles(ctx) + if err != nil { + return xerrors.Errorf("fetch site roles: %w", err) + } + + var siteRoles = make([]string, 0) + for _, role := range roles { + if role.Assignable { + siteRoles = append(siteRoles, role.Name) + } + } + + userRoles, err := client.UserRoles(ctx, user.Username) + if err != nil { + return xerrors.Errorf("fetch user roles: %w", err) + } + + 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 +} diff --git a/cli/userroles.go b/cli/userroles.go new file mode 100644 index 0000000000000..1716f92864e49 --- /dev/null +++ b/cli/userroles.go @@ -0,0 +1,62 @@ +package cli + +import ( + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" + "golang.org/x/xerrors" +) + +func (r *RootCmd) userEditRoles() *serpent.Command { + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "edit-roles ", + Short: "Edit a user's roles by username or id", + 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) + } + + roles, err := client.ListSiteRoles(ctx) + if err != nil { + return xerrors.Errorf("fetch site roles: %w", err) + } + + var siteRoles = make([]string, 0) + for _, role := range roles { + if role.Assignable { + siteRoles = append(siteRoles, role.Name) + } + } + + userRoles, err := client.UserRoles(ctx, user.Username) + if err != nil { + return xerrors.Errorf("fetch user roles: %w", err) + } + + 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 +} diff --git a/cli/users.go b/cli/users.go index 04876921ae5c7..fa15fcddad0ee 100644 --- a/cli/users.go +++ b/cli/users.go @@ -25,13 +25,3 @@ func (r *RootCmd) users() *serpent.Command { } return cmd } - -// An alias for `organization members edit-roles` for single-organization -// deployments. -func (r *RootCmd) userEditRoles() *serpent.Command { - orgContext := NewOrganizationContext() - cmd := r.assignOrganizationRoles(orgContext) - cmd.Short = "Edit a member's roles" - - return cmd -} From 2b2e8fe91edcceb7ec9671039873a27d0c596075 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 11 Apr 2025 17:25:44 +0000 Subject: [PATCH 05/14] fix: remove duplicate file --- cli/usereditroles.go | 1 - cli/userroles.go | 62 -------------------------------------------- 2 files changed, 63 deletions(-) delete mode 100644 cli/userroles.go diff --git a/cli/usereditroles.go b/cli/usereditroles.go index 4fe46b2497d8b..1716f92864e49 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -1,7 +1,6 @@ package cli import ( - // "fmt" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" diff --git a/cli/userroles.go b/cli/userroles.go deleted file mode 100644 index 1716f92864e49..0000000000000 --- a/cli/userroles.go +++ /dev/null @@ -1,62 +0,0 @@ -package cli - -import ( - "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/serpent" - "golang.org/x/xerrors" -) - -func (r *RootCmd) userEditRoles() *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ - Use: "edit-roles ", - Short: "Edit a user's roles by username or id", - 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) - } - - roles, err := client.ListSiteRoles(ctx) - if err != nil { - return xerrors.Errorf("fetch site roles: %w", err) - } - - var siteRoles = make([]string, 0) - for _, role := range roles { - if role.Assignable { - siteRoles = append(siteRoles, role.Name) - } - } - - userRoles, err := client.UserRoles(ctx, user.Username) - if err != nil { - return xerrors.Errorf("fetch user roles: %w", err) - } - - 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 -} From fc2ccf11ecb3d76d7cbe6e646b1bd8be38b928f0 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 11 Apr 2025 17:32:41 +0000 Subject: [PATCH 06/14] chore: make gen --- cli/testdata/coder_users_--help.golden | 2 +- cli/testdata/coder_users_edit-roles_--help.golden | 6 ++---- docs/manifest.json | 2 +- docs/reference/cli/users.md | 2 +- docs/reference/cli/users_edit-roles.md | 8 ++------ 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/cli/testdata/coder_users_--help.golden b/cli/testdata/coder_users_--help.golden index 9f178ec281c00..585588cbc6e18 100644 --- a/cli/testdata/coder_users_--help.golden +++ b/cli/testdata/coder_users_--help.golden @@ -12,7 +12,7 @@ SUBCOMMANDS: interact with the platform create delete Delete a user by username or user_id. - edit-roles Edit a member's roles + 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. diff --git a/cli/testdata/coder_users_edit-roles_--help.golden b/cli/testdata/coder_users_edit-roles_--help.golden index d6adc95b5a400..6c24f67c0988a 100644 --- a/cli/testdata/coder_users_edit-roles_--help.golden +++ b/cli/testdata/coder_users_edit-roles_--help.golden @@ -1,11 +1,9 @@ coder v0.0.0-devel USAGE: - coder users edit-roles [roles...] + coder users edit-roles - Edit a member's roles - - Aliases: edit-role + Edit a user's roles by username or id ——— Run `coder --help` for a list of global options. diff --git a/docs/manifest.json b/docs/manifest.json index c8e051182946a..be6fd393303f6 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1594,7 +1594,7 @@ }, { "title": "users edit-roles", - "description": "Edit a member's roles", + "description": "Edit a user's roles by username or id", "path": "reference/cli/users_edit-roles.md" }, { diff --git a/docs/reference/cli/users.md b/docs/reference/cli/users.md index d4d970ca1ada9..d942699d6ee31 100644 --- a/docs/reference/cli/users.md +++ b/docs/reference/cli/users.md @@ -21,6 +21,6 @@ coder users [subcommand] | [list](./users_list.md) | | | [show](./users_show.md) | Show a single user. Use 'me' to indicate the currently authenticated user. | | [delete](./users_delete.md) | Delete a user by username or user_id. | -| [edit-roles](./users_edit-roles.md) | Edit a member's roles | +| [edit-roles](./users_edit-roles.md) | Edit a user's roles by username or id | | [activate](./users_activate.md) | Update a user's status to 'active'. Active users can fully interact with the platform | | [suspend](./users_suspend.md) | Update a user's status to 'suspended'. A suspended user cannot log into the platform | diff --git a/docs/reference/cli/users_edit-roles.md b/docs/reference/cli/users_edit-roles.md index cc08f01fb2fb5..b8bcf190ac675 100644 --- a/docs/reference/cli/users_edit-roles.md +++ b/docs/reference/cli/users_edit-roles.md @@ -1,14 +1,10 @@ # users edit-roles -Edit a member's roles - -Aliases: - -* edit-role +Edit a user's roles by username or id ## Usage ```console -coder users edit-roles [roles...] +coder users edit-roles ``` From 42bb16df547e30c02d09ff3fa955c7a228dcf602 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 11 Apr 2025 18:30:13 +0000 Subject: [PATCH 07/14] fix: sort roles so they always appear in the same order --- cli/usereditroles.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/usereditroles.go b/cli/usereditroles.go index 1716f92864e49..930fac3850200 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -1,6 +1,8 @@ package cli import ( + "sort" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" @@ -38,6 +40,7 @@ func (r *RootCmd) userEditRoles() *serpent.Command { return xerrors.Errorf("fetch user roles: %w", err) } + sort.Strings(siteRoles) selectedRoles, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{ Message: "Select the roles you'd like to assign to the user", Options: siteRoles, From 45894ee4fb7d6b29f32a6cf8f2b29255b8fefcef Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 11 Apr 2025 20:28:24 +0000 Subject: [PATCH 08/14] chore: fmt --- cli/usereditroles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/usereditroles.go b/cli/usereditroles.go index 930fac3850200..1d39c387a7f08 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -28,7 +28,7 @@ func (r *RootCmd) userEditRoles() *serpent.Command { return xerrors.Errorf("fetch site roles: %w", err) } - var siteRoles = make([]string, 0) + siteRoles := make([]string, 0) for _, role := range roles { if role.Assignable { siteRoles = append(siteRoles, role.Name) From 1e049b31adcca3eaafdefd926822c118dcb4f018 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 11 Apr 2025 20:30:43 +0000 Subject: [PATCH 09/14] chore: fmt --- cli/usereditroles.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/usereditroles.go b/cli/usereditroles.go index 1d39c387a7f08..3328679b93da2 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -3,10 +3,11 @@ package cli import ( "sort" + "golang.org/x/xerrors" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" - "golang.org/x/xerrors" ) func (r *RootCmd) userEditRoles() *serpent.Command { From 681ecba2c54e2bfa7b84d3a96aa75229ad7e094b Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Tue, 15 Apr 2025 19:30:40 +0000 Subject: [PATCH 10/14] feat: add roles flag to user edit-roles command for passing in roles --- cli/usereditroles.go | 54 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/cli/usereditroles.go b/cli/usereditroles.go index 3328679b93da2..b6a6f7791aed6 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -1,7 +1,9 @@ package cli import ( + "slices" "sort" + "strings" "golang.org/x/xerrors" @@ -12,9 +14,20 @@ import ( func (r *RootCmd) userEditRoles() *serpent.Command { client := new(codersdk.Client) + + var givenRoles []string + cmd := &serpent.Command{ - Use: "edit-roles ", - Short: "Edit a user's roles by username or id", + Use: "edit-roles ", + Short: "Edit a user's roles by username or id", + Options: []serpent.Option{ + cliui.SkipPromptOption(), + { + Name: "roles", + Description: "A list of roles to give to the user. This removes any existing roles the user may have.", + Flag: "roles", + Value: serpent.StringArrayOf(&givenRoles)}, + }, Middleware: serpent.Chain(serpent.RequireNArgs(1), r.InitClient(client)), Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() @@ -35,20 +48,41 @@ func (r *RootCmd) userEditRoles() *serpent.Command { siteRoles = append(siteRoles, role.Name) } } + sort.Strings(siteRoles) userRoles, err := client.UserRoles(ctx, user.Username) if err != nil { return xerrors.Errorf("fetch user roles: %w", err) } - sort.Strings(siteRoles) - 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) + var selectedRoles []string + if len(givenRoles) > 0 { + // If the none role is present ignore all other roles. + // This is so there is a way to clear roles from the CLI without making a + // new command. + if slices.Contains(givenRoles, "none") { + selectedRoles = []string{} + } else { + // 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, or none\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{ From a2b349df5b386e7ba35a15a62a41063191db249c Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Tue, 15 Apr 2025 20:13:56 +0000 Subject: [PATCH 11/14] fix: use static rbac method for getting site roles instead of API call --- cli/usereditroles.go | 46 ++++++++++++----------------- cli/usereditroles_test.go | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 cli/usereditroles_test.go diff --git a/cli/usereditroles.go b/cli/usereditroles.go index b6a6f7791aed6..af7278ead2546 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "slices" "sort" "strings" @@ -8,6 +9,7 @@ import ( "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" ) @@ -15,6 +17,14 @@ import ( 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{ @@ -24,7 +34,7 @@ func (r *RootCmd) userEditRoles() *serpent.Command { cliui.SkipPromptOption(), { Name: "roles", - Description: "A list of roles to give to the user. This removes any existing roles the user may have.", + 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)}, }, @@ -37,19 +47,6 @@ func (r *RootCmd) userEditRoles() *serpent.Command { return xerrors.Errorf("fetch user: %w", err) } - roles, err := client.ListSiteRoles(ctx) - if err != nil { - return xerrors.Errorf("fetch site roles: %w", err) - } - - siteRoles := make([]string, 0) - for _, role := range roles { - if role.Assignable { - siteRoles = append(siteRoles, role.Name) - } - } - sort.Strings(siteRoles) - userRoles, err := client.UserRoles(ctx, user.Username) if err != nil { return xerrors.Errorf("fetch user roles: %w", err) @@ -57,22 +54,15 @@ func (r *RootCmd) userEditRoles() *serpent.Command { var selectedRoles []string if len(givenRoles) > 0 { - // If the none role is present ignore all other roles. - // This is so there is a way to clear roles from the CLI without making a - // new command. - if slices.Contains(givenRoles, "none") { - selectedRoles = []string{} - } else { - // 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, or none\n", givenRole, siteRolesPretty) - } + // 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 } + + selectedRoles = givenRoles } else { selectedRoles, err = cliui.MultiSelect(inv, cliui.MultiSelectOptions{ Message: "Select the roles you'd like to assign to the user", diff --git a/cli/usereditroles_test.go b/cli/usereditroles_test.go new file mode 100644 index 0000000000000..49fd46ee9a8c0 --- /dev/null +++ b/cli/usereditroles_test.go @@ -0,0 +1,61 @@ +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) + _, 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, client, 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") + }) +} From ea2e9089d2e88c1007ae2b93b3efecf23719f7a9 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Tue, 15 Apr 2025 20:23:50 +0000 Subject: [PATCH 12/14] chore: fmt --- cli/usereditroles.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/usereditroles.go b/cli/usereditroles.go index af7278ead2546..f94c2e25dd0b5 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -36,7 +36,8 @@ func (r *RootCmd) userEditRoles() *serpent.Command { 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)}, + Value: serpent.StringArrayOf(&givenRoles), + }, }, Middleware: serpent.Chain(serpent.RequireNArgs(1), r.InitClient(client)), Handler: func(inv *serpent.Invocation) error { From c4d380609ce61fa499e5e2193059c0ee7db04fc3 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Tue, 15 Apr 2025 20:29:59 +0000 Subject: [PATCH 13/14] chore: lint --- cli/usereditroles.go | 1 - cli/usereditroles_test.go | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/usereditroles.go b/cli/usereditroles.go index f94c2e25dd0b5..815d8f47dc186 100644 --- a/cli/usereditroles.go +++ b/cli/usereditroles.go @@ -73,7 +73,6 @@ func (r *RootCmd) userEditRoles() *serpent.Command { if err != nil { return xerrors.Errorf("selecting roles for user: %w", err) } - } _, err = client.UpdateUserRoles(ctx, user.Username, codersdk.UpdateRoles{ diff --git a/cli/usereditroles_test.go b/cli/usereditroles_test.go index 49fd46ee9a8c0..bd12092501808 100644 --- a/cli/usereditroles_test.go +++ b/cli/usereditroles_test.go @@ -23,10 +23,11 @@ func TestUserEditRoles(t *testing.T) { 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, client, root) + clitest.SetupConfig(t, userAdmin, root) // Create context with timeout ctx := testutil.Context(t, testutil.WaitShort) From 35ed6adc9b7228fd2f256b593af07dcf12513cb7 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Tue, 15 Apr 2025 20:40:47 +0000 Subject: [PATCH 14/14] chore: make gen --- .../coder_users_edit-roles_--help.golden | 11 +++++++++- docs/reference/cli/users_edit-roles.md | 20 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cli/testdata/coder_users_edit-roles_--help.golden b/cli/testdata/coder_users_edit-roles_--help.golden index 6c24f67c0988a..02dd9155b4d4e 100644 --- a/cli/testdata/coder_users_edit-roles_--help.golden +++ b/cli/testdata/coder_users_edit-roles_--help.golden @@ -1,9 +1,18 @@ coder v0.0.0-devel USAGE: - coder users edit-roles + coder users edit-roles [flags] 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. diff --git a/docs/reference/cli/users_edit-roles.md b/docs/reference/cli/users_edit-roles.md index b8bcf190ac675..23e0baa42afff 100644 --- a/docs/reference/cli/users_edit-roles.md +++ b/docs/reference/cli/users_edit-roles.md @@ -6,5 +6,23 @@ Edit a user's roles by username or id ## Usage ```console -coder users edit-roles +coder users edit-roles [flags] ``` + +## Options + +### -y, --yes + +| | | +|------|-------------------| +| Type | bool | + +Bypass prompts. + +### --roles + +| | | +|------|---------------------------| +| Type | 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.