From 7d51515f9dd37c6a71138425bc7bca3e65a54c2f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Jun 2024 09:49:32 -1000 Subject: [PATCH] chore: implement assign organization roles from the cli (#13558) Basic functionality to assign roles to an organization member via cli. --- cli/organizationmembers.go | 66 +++++++++++++++++++++- cli/organizationmembers_test.go | 2 +- enterprise/cli/organizationmembers_test.go | 56 +++++++++++++++++- 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/cli/organizationmembers.go b/cli/organizationmembers.go index 58138e65a3..d81f08f333 100644 --- a/cli/organizationmembers.go +++ b/cli/organizationmembers.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strings" "golang.org/x/xerrors" @@ -11,6 +12,66 @@ import ( ) func (r *RootCmd) organizationMembers() *serpent.Command { + cmd := &serpent.Command{ + Use: "members", + Aliases: []string{"member"}, + Short: "Manage organization members", + Children: []*serpent.Command{ + r.listOrganizationMembers(), + r.assignOrganizationRoles(), + }, + Handler: func(inv *serpent.Invocation) error { + return inv.Command.HelpHandler(inv) + }, + } + + return cmd +} + +func (r *RootCmd) assignOrganizationRoles() *serpent.Command { + client := new(codersdk.Client) + + cmd := &serpent.Command{ + Use: "edit-roles [roles...]", + Aliases: []string{"edit-role"}, + Short: "Edit organization member's roles", + Middleware: serpent.Chain( + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + organization, err := CurrentOrganization(r, inv, client) + if err != nil { + return err + } + + if len(inv.Args) < 1 { + return xerrors.Errorf("user_id or username is required as the first argument") + } + userIdentifier := inv.Args[0] + roles := inv.Args[1:] + + member, err := client.UpdateOrganizationMemberRoles(ctx, organization.ID, userIdentifier, codersdk.UpdateRoles{ + Roles: roles, + }) + if err != nil { + return xerrors.Errorf("update member roles: %w", err) + } + + updatedTo := make([]string, 0) + for _, role := range member.Roles { + updatedTo = append(updatedTo, role.String()) + } + + _, _ = fmt.Fprintf(inv.Stdout, "Member roles updated to [%s]\n", strings.Join(updatedTo, ", ")) + return nil + }, + } + + return cmd +} + +func (r *RootCmd) listOrganizationMembers() *serpent.Command { formatter := cliui.NewOutputFormatter( cliui.TableFormat([]codersdk.OrganizationMemberWithName{}, []string{"username", "organization_roles"}), cliui.JSONFormat(), @@ -18,9 +79,8 @@ func (r *RootCmd) organizationMembers() *serpent.Command { client := new(codersdk.Client) cmd := &serpent.Command{ - Use: "members", - Short: "List all organization members", - Aliases: []string{"member"}, + Use: "list", + Short: "List all organization members", Middleware: serpent.Chain( serpent.RequireNArgs(0), r.InitClient(client), diff --git a/cli/organizationmembers_test.go b/cli/organizationmembers_test.go index 077ec0e00a..6cd8b9d3cc 100644 --- a/cli/organizationmembers_test.go +++ b/cli/organizationmembers_test.go @@ -23,7 +23,7 @@ func TestListOrganizationMembers(t *testing.T) { client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin()) ctx := testutil.Context(t, testutil.WaitMedium) - inv, root := clitest.New(t, "organization", "members", "-c", "user_id,username,roles") + inv, root := clitest.New(t, "organization", "members", "list", "-c", "user_id,username,roles") clitest.SetupConfig(t, client, root) buf := new(bytes.Buffer) diff --git a/enterprise/cli/organizationmembers_test.go b/enterprise/cli/organizationmembers_test.go index b308c4f249..fb6a7cb286 100644 --- a/enterprise/cli/organizationmembers_test.go +++ b/enterprise/cli/organizationmembers_test.go @@ -53,7 +53,7 @@ func TestEnterpriseListOrganizationMembers(t *testing.T) { OrganizationID: owner.OrganizationID, }, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) - inv, root := clitest.New(t, "organization", "members", "-c", "user_id,username,organization_roles") + inv, root := clitest.New(t, "organization", "members", "list", "-c", "user_id,username,organization_roles") clitest.SetupConfig(t, client, root) buf := new(bytes.Buffer) @@ -66,3 +66,57 @@ func TestEnterpriseListOrganizationMembers(t *testing.T) { require.Contains(t, buf.String(), customRole.DisplayName) }) } + +func TestAssignOrganizationMemberRole(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)} + + ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + }, + }, + }) + _, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin()) + + ctx := testutil.Context(t, testutil.WaitMedium) + // nolint:gocritic // requires owner role to create + customRole, err := ownerClient.PatchOrganizationRole(ctx, owner.OrganizationID, codersdk.Role{ + Name: "custom-role", + OrganizationID: owner.OrganizationID.String(), + DisplayName: "Custom Role", + SitePermissions: nil, + OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + codersdk.ResourceWorkspace: {codersdk.ActionRead}, + }), + UserPermissions: nil, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, "organization", "members", "edit-roles", user.Username, codersdk.RoleOrganizationAdmin, customRole.Name) + // nolint:gocritic // you cannot change your own roles + clitest.SetupConfig(t, ownerClient, root) + + buf := new(bytes.Buffer) + inv.Stdout = buf + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + require.Contains(t, buf.String(), must(rbac.RoleByName(rbac.ScopedRoleOrgAdmin(owner.OrganizationID))).DisplayName) + require.Contains(t, buf.String(), customRole.DisplayName) + }) +} + +func must[V any](v V, err error) V { + if err != nil { + panic(err) + } + return v +}