diff --git a/cli/sharing.go b/cli/sharing.go index f824a0a4c8e20..aa1678e7a9e81 100644 --- a/cli/sharing.go +++ b/cli/sharing.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "regexp" @@ -13,12 +14,6 @@ import ( const defaultGroupDisplay = "-" -type workspaceShareRow struct { - User string `table:"user"` - Group string `table:"group,default_sort"` - Role codersdk.WorkspaceRole `table:"role"` -} - func (r *RootCmd) sharing() *serpent.Command { orgContext := NewOrganizationContext() @@ -29,14 +24,52 @@ func (r *RootCmd) sharing() *serpent.Command { Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) }, - Children: []*serpent.Command{r.shareWorkspace(orgContext)}, - Hidden: true, + Children: []*serpent.Command{ + r.shareWorkspace(orgContext), + r.statusWorkspaceSharing(), + }, + Hidden: true, } orgContext.AttachOptions(cmd) return cmd } +func (r *RootCmd) statusWorkspaceSharing() *serpent.Command { + client := new(codersdk.Client) + + cmd := &serpent.Command{ + Use: "status ", + Short: "List all users and groups the given Workspace is shared with.", + Aliases: []string{"list"}, + Middleware: serpent.Chain( + r.InitClient(client), + serpent.RequireNArgs(1), + ), + Handler: func(inv *serpent.Invocation) error { + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) + if err != nil { + return xerrors.Errorf("unable to fetch Workspace %s: %w", inv.Args[0], err) + } + + acl, err := client.WorkspaceACL(inv.Context(), workspace.ID) + if err != nil { + return xerrors.Errorf("unable to fetch ACL for Workspace: %w", err) + } + + out, err := workspaceACLToTable(inv.Context(), &acl) + if err != nil { + return err + } + + _, err = fmt.Fprintln(inv.Stdout, out) + return err + }, + } + + return cmd +} + func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Command { var ( // Username regex taken from codersdk/name.go @@ -44,11 +77,6 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma client = new(codersdk.Client) users []string groups []string - formatter = cliui.NewOutputFormatter( - cliui.TableFormat( - []workspaceShareRow{}, []string{"User", "Group", "Role"}), - cliui.JSONFormat(), - ) ) cmd := &serpent.Command{ @@ -175,37 +203,12 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma return err } - workspaceACL, err := client.WorkspaceACL(inv.Context(), workspace.ID) + acl, err := client.WorkspaceACL(inv.Context(), workspace.ID) if err != nil { return xerrors.Errorf("could not fetch current workspace ACL after sharing %w", err) } - outputRows := make([]workspaceShareRow, 0) - for _, user := range workspaceACL.Users { - if user.Role == codersdk.WorkspaceRoleDeleted { - continue - } - - outputRows = append(outputRows, workspaceShareRow{ - User: user.Username, - Group: defaultGroupDisplay, - Role: user.Role, - }) - } - for _, group := range workspaceACL.Groups { - if group.Role == codersdk.WorkspaceRoleDeleted { - continue - } - - for _, user := range group.Members { - outputRows = append(outputRows, workspaceShareRow{ - User: user.Username, - Group: group.Name, - Role: group.Role, - }) - } - } - out, err := formatter.Format(inv.Context(), outputRows) + out, err := workspaceACLToTable(inv.Context(), &acl) if err != nil { return err } @@ -229,3 +232,48 @@ func stringToWorkspaceRole(role string) (codersdk.WorkspaceRole, error) { role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse) } } + +func workspaceACLToTable(ctx context.Context, acl *codersdk.WorkspaceACL) (string, error) { + type workspaceShareRow struct { + User string `table:"user"` + Group string `table:"group,default_sort"` + Role codersdk.WorkspaceRole `table:"role"` + } + + formatter := cliui.NewOutputFormatter( + cliui.TableFormat( + []workspaceShareRow{}, []string{"User", "Group", "Role"}), + cliui.JSONFormat()) + + outputRows := make([]workspaceShareRow, 0) + for _, user := range acl.Users { + if user.Role == codersdk.WorkspaceRoleDeleted { + continue + } + + outputRows = append(outputRows, workspaceShareRow{ + User: user.Username, + Group: defaultGroupDisplay, + Role: user.Role, + }) + } + for _, group := range acl.Groups { + if group.Role == codersdk.WorkspaceRoleDeleted { + continue + } + + for _, user := range group.Members { + outputRows = append(outputRows, workspaceShareRow{ + User: user.Username, + Group: group.Name, + Role: group.Role, + }) + } + } + out, err := formatter.Format(ctx, outputRows) + if err != nil { + return "", err + } + + return out, nil +} diff --git a/cli/sharing_test.go b/cli/sharing_test.go index 2da91afa75d16..01bfc0a83873a 100644 --- a/cli/sharing_test.go +++ b/cli/sharing_test.go @@ -168,3 +168,52 @@ func TestSharingShare(t *testing.T) { assert.True(t, found, fmt.Sprintf("expected to find the username %s and role %s in the command: %s", toShareWithUser.Username, codersdk.WorkspaceRoleAdmin, out.String())) }) } + +func TestSharingStatus(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)} + + t.Run("ListSharedUsers", func(t *testing.T) { + t.Parallel() + + var ( + client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{ + DeploymentValues: dv, + }) + orgOwner = coderdtest.CreateFirstUser(t, client) + workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID)) + workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: workspaceOwner.ID, + OrganizationID: orgOwner.OrganizationID, + }).Do().Workspace + _, toShareWithUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + ctx = testutil.Context(t, testutil.WaitMedium) + ) + + err := client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{ + UserRoles: map[string]codersdk.WorkspaceRole{ + toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse, + }, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, "sharing", "status", workspace.Name, "--org", orgOwner.OrganizationID.String()) + clitest.SetupConfig(t, workspaceOwnerClient, root) + + out := bytes.NewBuffer(nil) + inv.Stdout = out + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + found := false + for _, line := range strings.Split(out.String(), "\n") { + if strings.Contains(line, toShareWithUser.Username) && strings.Contains(line, string(codersdk.WorkspaceRoleUse)) { + found = true + break + } + } + assert.True(t, found, "expected to find username %s with role %s in the output: %s", toShareWithUser.Username, codersdk.WorkspaceRoleUse, out.String()) + }) +} diff --git a/enterprise/cli/sharing_test.go b/enterprise/cli/sharing_test.go index a03d77412d235..906c02a148d4b 100644 --- a/enterprise/cli/sharing_test.go +++ b/enterprise/cli/sharing_test.go @@ -187,6 +187,64 @@ func TestSharingShareEnterprise(t *testing.T) { }) } +func TestSharingStatus(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)} + + t.Run("ListSharedUsers", func(t *testing.T) { + t.Parallel() + + var ( + client, db, orgOwner = coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID)) + workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: workspaceOwner.ID, + OrganizationID: orgOwner.OrganizationID, + }).Do().Workspace + _, orgMember = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + ctx = testutil.Context(t, testutil.WaitMedium) + ) + + group, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "new-group", []uuid.UUID{orgMember.ID}) + require.NoError(t, err) + + err = client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{ + GroupRoles: map[string]codersdk.WorkspaceRole{ + group.ID.String(): codersdk.WorkspaceRoleUse, + }, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, "sharing", "status", workspace.Name, "--org", orgOwner.OrganizationID.String()) + clitest.SetupConfig(t, workspaceOwnerClient, root) + + out := bytes.NewBuffer(nil) + inv.Stdout = out + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + found := false + for _, line := range strings.Split(out.String(), "\n") { + if strings.Contains(line, orgMember.Username) && strings.Contains(line, string(codersdk.WorkspaceRoleUse)) && strings.Contains(line, group.Name) { + found = true + break + } + } + assert.True(t, found, "expected to find username %s with role %s in the output: %s", orgMember.Username, codersdk.WorkspaceRoleUse, out.String()) + }) +} + func createGroupWithMembers(ctx context.Context, client *codersdk.Client, orgID uuid.UUID, name string, memberIDs []uuid.UUID) (codersdk.Group, error) { group, err := client.CreateGroup(ctx, orgID, codersdk.CreateGroupRequest{ Name: name,