Skip to content

Commit 065c7c3

Browse files
authored
feat: add sharing show command to the CLI (#19707)
Closes coder/internal#860
1 parent 9f66395 commit 065c7c3

File tree

3 files changed

+195
-40
lines changed

3 files changed

+195
-40
lines changed

cli/sharing.go

Lines changed: 88 additions & 40 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

@@ -13,12 +14,6 @@ import (
1314

1415
const defaultGroupDisplay = "-"
1516

16-
type workspaceShareRow struct {
17-
User string `table:"user"`
18-
Group string `table:"group,default_sort"`
19-
Role codersdk.WorkspaceRole `table:"role"`
20-
}
21-
2217
func (r *RootCmd) sharing() *serpent.Command {
2318
orgContext := NewOrganizationContext()
2419

@@ -29,26 +24,59 @@ func (r *RootCmd) sharing() *serpent.Command {
2924
Handler: func(inv *serpent.Invocation) error {
3025
return inv.Command.HelpHandler(inv)
3126
},
32-
Children: []*serpent.Command{r.shareWorkspace(orgContext)},
33-
Hidden: true,
27+
Children: []*serpent.Command{
28+
r.shareWorkspace(orgContext),
29+
r.statusWorkspaceSharing(),
30+
},
31+
Hidden: true,
3432
}
3533

3634
orgContext.AttachOptions(cmd)
3735
return cmd
3836
}
3937

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

5482
cmd := &serpent.Command{
@@ -175,37 +203,12 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma
175203
return err
176204
}
177205

178-
workspaceACL, err := client.WorkspaceACL(inv.Context(), workspace.ID)
206+
acl, err := client.WorkspaceACL(inv.Context(), workspace.ID)
179207
if err != nil {
180208
return xerrors.Errorf("could not fetch current workspace ACL after sharing %w", err)
181209
}
182210

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)
211+
out, err := workspaceACLToTable(inv.Context(), &acl)
209212
if err != nil {
210213
return err
211214
}
@@ -229,3 +232,48 @@ func stringToWorkspaceRole(role string) (codersdk.WorkspaceRole, error) {
229232
role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse)
230233
}
231234
}
235+
236+
func workspaceACLToTable(ctx context.Context, acl *codersdk.WorkspaceACL) (string, error) {
237+
type workspaceShareRow struct {
238+
User string `table:"user"`
239+
Group string `table:"group,default_sort"`
240+
Role codersdk.WorkspaceRole `table:"role"`
241+
}
242+
243+
formatter := cliui.NewOutputFormatter(
244+
cliui.TableFormat(
245+
[]workspaceShareRow{}, []string{"User", "Group", "Role"}),
246+
cliui.JSONFormat())
247+
248+
outputRows := make([]workspaceShareRow, 0)
249+
for _, user := range acl.Users {
250+
if user.Role == codersdk.WorkspaceRoleDeleted {
251+
continue
252+
}
253+
254+
outputRows = append(outputRows, workspaceShareRow{
255+
User: user.Username,
256+
Group: defaultGroupDisplay,
257+
Role: user.Role,
258+
})
259+
}
260+
for _, group := range acl.Groups {
261+
if group.Role == codersdk.WorkspaceRoleDeleted {
262+
continue
263+
}
264+
265+
for _, user := range group.Members {
266+
outputRows = append(outputRows, workspaceShareRow{
267+
User: user.Username,
268+
Group: group.Name,
269+
Role: group.Role,
270+
})
271+
}
272+
}
273+
out, err := formatter.Format(ctx, outputRows)
274+
if err != nil {
275+
return "", err
276+
}
277+
278+
return out, nil
279+
}

cli/sharing_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,52 @@ 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+
break
215+
}
216+
}
217+
assert.True(t, found, "expected to find username %s with role %s in the output: %s", toShareWithUser.Username, codersdk.WorkspaceRoleUse, out.String())
218+
})
219+
}

enterprise/cli/sharing_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,64 @@ 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+
break
242+
}
243+
}
244+
assert.True(t, found, "expected to find username %s with role %s in the output: %s", orgMember.Username, codersdk.WorkspaceRoleUse, out.String())
245+
})
246+
}
247+
190248
func createGroupWithMembers(ctx context.Context, client *codersdk.Client, orgID uuid.UUID, name string, memberIDs []uuid.UUID) (codersdk.Group, error) {
191249
group, err := client.CreateGroup(ctx, orgID, codersdk.CreateGroupRequest{
192250
Name: name,

0 commit comments

Comments
 (0)