Skip to content

Commit 6de1a82

Browse files
committed
feat: add sharing show command to the CLI
1 parent 909acbc commit 6de1a82

File tree

3 files changed

+201
-34
lines changed

3 files changed

+201
-34
lines changed

cli/sharing.go

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"context"
45
"fmt"
56
"regexp"
67

@@ -29,26 +30,59 @@ func (r *RootCmd) sharing() *serpent.Command {
2930
Handler: func(inv *serpent.Invocation) error {
3031
return inv.Command.HelpHandler(inv)
3132
},
32-
Children: []*serpent.Command{r.shareWorkspace(orgContext)},
33-
Hidden: true,
33+
Children: []*serpent.Command{
34+
r.shareWorkspace(orgContext),
35+
r.statusWorkspaceSharing(),
36+
},
37+
Hidden: true,
3438
}
3539

3640
orgContext.AttachOptions(cmd)
3741
return cmd
3842
}
3943

44+
func (r *RootCmd) statusWorkspaceSharing() *serpent.Command {
45+
client := new(codersdk.Client)
46+
47+
cmd := &serpent.Command{
48+
Use: "status <workspace>",
49+
Short: "List all users and groups the given Workspace is shared with.",
50+
Aliases: []string{"list"},
51+
Middleware: serpent.Chain(
52+
r.InitClient(client),
53+
serpent.RequireNArgs(1),
54+
),
55+
Handler: func(inv *serpent.Invocation) error {
56+
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
57+
if err != nil {
58+
return xerrors.Errorf("unable to fetch Workspace %s: %w", inv.Args[0], err)
59+
}
60+
61+
acl, err := client.WorkspaceACL(inv.Context(), workspace.ID)
62+
if err != nil {
63+
return xerrors.Errorf("unable to fetch ACL for Workspace: %w", err)
64+
}
65+
66+
out, err := workspaceACLToTable(inv.Context(), &acl)
67+
if err != nil {
68+
return err
69+
}
70+
71+
_, err = fmt.Fprintln(inv.Stdout, out)
72+
return err
73+
},
74+
}
75+
76+
return cmd
77+
}
78+
4079
func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Command {
4180
var (
4281
// Username regex taken from codersdk/name.go
4382
nameRoleRegex = regexp.MustCompile(`(^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)+(?::([A-Za-z0-9-]+))?`)
4483
client = new(codersdk.Client)
4584
users []string
4685
groups []string
47-
formatter = cliui.NewOutputFormatter(
48-
cliui.TableFormat(
49-
[]workspaceShareRow{}, []string{"User", "Group", "Role"}),
50-
cliui.JSONFormat(),
51-
)
5286
)
5387

5488
cmd := &serpent.Command{
@@ -175,37 +209,12 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma
175209
return err
176210
}
177211

178-
workspaceACL, err := client.WorkspaceACL(inv.Context(), workspace.ID)
212+
acl, err := client.WorkspaceACL(inv.Context(), workspace.ID)
179213
if err != nil {
180214
return xerrors.Errorf("could not fetch current workspace ACL after sharing %w", err)
181215
}
182216

183-
outputRows := make([]workspaceShareRow, 0)
184-
for _, user := range workspaceACL.Users {
185-
if user.Role == codersdk.WorkspaceRoleDeleted {
186-
continue
187-
}
188-
189-
outputRows = append(outputRows, workspaceShareRow{
190-
User: user.Username,
191-
Group: defaultGroupDisplay,
192-
Role: user.Role,
193-
})
194-
}
195-
for _, group := range workspaceACL.Groups {
196-
if group.Role == codersdk.WorkspaceRoleDeleted {
197-
continue
198-
}
199-
200-
for _, user := range group.Members {
201-
outputRows = append(outputRows, workspaceShareRow{
202-
User: user.Username,
203-
Group: group.Name,
204-
Role: group.Role,
205-
})
206-
}
207-
}
208-
out, err := formatter.Format(inv.Context(), outputRows)
217+
out, err := workspaceACLToTable(inv.Context(), &acl)
209218
if err != nil {
210219
return err
211220
}
@@ -229,3 +238,48 @@ func stringToWorkspaceRole(role string) (codersdk.WorkspaceRole, error) {
229238
role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse)
230239
}
231240
}
241+
242+
func workspaceACLToTable(ctx context.Context, acl *codersdk.WorkspaceACL) (string, error) {
243+
type workspaceShareRow struct {
244+
User string `table:"user"`
245+
Group string `table:"group,default_sort"`
246+
Role codersdk.WorkspaceRole `table:"role"`
247+
}
248+
249+
formatter := cliui.NewOutputFormatter(
250+
cliui.TableFormat(
251+
[]workspaceShareRow{}, []string{"User", "Group", "Role"}),
252+
cliui.JSONFormat())
253+
254+
outputRows := make([]workspaceShareRow, 0)
255+
for _, user := range acl.Users {
256+
if user.Role == codersdk.WorkspaceRoleDeleted {
257+
continue
258+
}
259+
260+
outputRows = append(outputRows, workspaceShareRow{
261+
User: user.Username,
262+
Group: defaultGroupDisplay,
263+
Role: user.Role,
264+
})
265+
}
266+
for _, group := range acl.Groups {
267+
if group.Role == codersdk.WorkspaceRoleDeleted {
268+
continue
269+
}
270+
271+
for _, user := range group.Members {
272+
outputRows = append(outputRows, workspaceShareRow{
273+
User: user.Username,
274+
Group: group.Name,
275+
Role: group.Role,
276+
})
277+
}
278+
}
279+
out, err := formatter.Format(ctx, outputRows)
280+
if err != nil {
281+
return "", err
282+
}
283+
284+
return out, nil
285+
}

cli/sharing_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,55 @@ func TestSharingShare(t *testing.T) {
168168
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()))
169169
})
170170
}
171+
172+
func TestSharingStatus(t *testing.T) {
173+
t.Parallel()
174+
175+
dv := coderdtest.DeploymentValues(t)
176+
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)}
177+
178+
t.Run("ListSharedUsers", func(t *testing.T) {
179+
t.Parallel()
180+
181+
var (
182+
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
183+
DeploymentValues: dv,
184+
})
185+
orgOwner = coderdtest.CreateFirstUser(t, client)
186+
workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID))
187+
workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
188+
OwnerID: workspaceOwner.ID,
189+
OrganizationID: orgOwner.OrganizationID,
190+
}).Do().Workspace
191+
_, toShareWithUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID)
192+
ctx = testutil.Context(t, testutil.WaitMedium)
193+
)
194+
195+
err := client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{
196+
UserRoles: map[string]codersdk.WorkspaceRole{
197+
toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse,
198+
},
199+
})
200+
require.NoError(t, err)
201+
202+
inv, root := clitest.New(t, "sharing", "status", workspace.Name, "--org", orgOwner.OrganizationID.String())
203+
clitest.SetupConfig(t, workspaceOwnerClient, root)
204+
205+
out := bytes.NewBuffer(nil)
206+
inv.Stdout = out
207+
err = inv.WithContext(ctx).Run()
208+
require.NoError(t, err)
209+
210+
found := false
211+
for _, line := range strings.Split(out.String(), "\n") {
212+
if strings.Contains(line, toShareWithUser.Username) && strings.Contains(line, string(codersdk.WorkspaceRoleUse)) {
213+
found = true
214+
}
215+
216+
if found {
217+
break
218+
}
219+
}
220+
assert.True(t, found, "expected to find username %s with role %s in the output: %s", toShareWithUser.Username, codersdk.WorkspaceRoleUse, out.String())
221+
})
222+
}

enterprise/cli/sharing_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,67 @@ func TestSharingShareEnterprise(t *testing.T) {
187187
})
188188
}
189189

190+
func TestSharingStatus(t *testing.T) {
191+
t.Parallel()
192+
193+
dv := coderdtest.DeploymentValues(t)
194+
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)}
195+
196+
t.Run("ListSharedUsers", func(t *testing.T) {
197+
t.Parallel()
198+
199+
var (
200+
client, db, orgOwner = coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
201+
Options: &coderdtest.Options{
202+
DeploymentValues: dv,
203+
},
204+
LicenseOptions: &coderdenttest.LicenseOptions{
205+
Features: license.Features{
206+
codersdk.FeatureTemplateRBAC: 1,
207+
},
208+
},
209+
})
210+
workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID))
211+
workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
212+
OwnerID: workspaceOwner.ID,
213+
OrganizationID: orgOwner.OrganizationID,
214+
}).Do().Workspace
215+
_, orgMember = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID)
216+
ctx = testutil.Context(t, testutil.WaitMedium)
217+
)
218+
219+
group, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "new-group", []uuid.UUID{orgMember.ID})
220+
require.NoError(t, err)
221+
222+
err = client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{
223+
GroupRoles: map[string]codersdk.WorkspaceRole{
224+
group.ID.String(): codersdk.WorkspaceRoleUse,
225+
},
226+
})
227+
require.NoError(t, err)
228+
229+
inv, root := clitest.New(t, "sharing", "status", workspace.Name, "--org", orgOwner.OrganizationID.String())
230+
clitest.SetupConfig(t, workspaceOwnerClient, root)
231+
232+
out := bytes.NewBuffer(nil)
233+
inv.Stdout = out
234+
err = inv.WithContext(ctx).Run()
235+
require.NoError(t, err)
236+
237+
found := false
238+
for _, line := range strings.Split(out.String(), "\n") {
239+
if strings.Contains(line, orgMember.Username) && strings.Contains(line, string(codersdk.WorkspaceRoleUse)) && strings.Contains(line, group.Name) {
240+
found = true
241+
}
242+
243+
if found {
244+
break
245+
}
246+
}
247+
assert.True(t, found, "expected to find username %s with role %s in the output: %s", orgMember.Username, codersdk.WorkspaceRoleUse, out.String())
248+
})
249+
}
250+
190251
func createGroupWithMembers(ctx context.Context, client *codersdk.Client, orgID uuid.UUID, name string, memberIDs []uuid.UUID) (codersdk.Group, error) {
191252
group, err := client.CreateGroup(ctx, orgID, codersdk.CreateGroupRequest{
192253
Name: name,

0 commit comments

Comments
 (0)