Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8d5c566

Browse files
authored
feat: add sharing remove command to the CLI (#19767)
Closes [coder/internal#861](coder/internal#861)
1 parent d5a02d5 commit 8d5c566

File tree

4 files changed

+509
-106
lines changed

4 files changed

+509
-106
lines changed

cli/sharing.go

Lines changed: 220 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
"golang.org/x/xerrors"
99

10+
"github.com/google/uuid"
11+
1012
"github.com/coder/coder/v2/cli/cliui"
1113
"github.com/coder/coder/v2/codersdk"
1214
"github.com/coder/serpent"
@@ -15,8 +17,6 @@ import (
1517
const defaultGroupDisplay = "-"
1618

1719
func (r *RootCmd) sharing() *serpent.Command {
18-
orgContext := NewOrganizationContext()
19-
2020
cmd := &serpent.Command{
2121
Use: "sharing [subcommand]",
2222
Short: "Commands for managing shared workspaces",
@@ -25,13 +25,13 @@ func (r *RootCmd) sharing() *serpent.Command {
2525
return inv.Command.HelpHandler(inv)
2626
},
2727
Children: []*serpent.Command{
28-
r.shareWorkspace(orgContext),
28+
r.shareWorkspace(),
29+
r.unshareWorkspace(),
2930
r.statusWorkspaceSharing(),
3031
},
3132
Hidden: true,
3233
}
3334

34-
orgContext.AttachOptions(cmd)
3535
return cmd
3636
}
3737

@@ -70,13 +70,14 @@ func (r *RootCmd) statusWorkspaceSharing() *serpent.Command {
7070
return cmd
7171
}
7272

73-
func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Command {
73+
func (r *RootCmd) shareWorkspace() *serpent.Command {
7474
var (
75+
client = new(codersdk.Client)
76+
users []string
77+
groups []string
78+
7579
// Username regex taken from codersdk/name.go
7680
nameRoleRegex = regexp.MustCompile(`(^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)+(?::([A-Za-z0-9-]+))?`)
77-
client = new(codersdk.Client)
78-
users []string
79-
groups []string
8081
)
8182

8283
cmd := &serpent.Command{
@@ -110,89 +111,130 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma
110111
return xerrors.Errorf("could not fetch the workspace %s: %w", inv.Args[0], err)
111112
}
112113

113-
org, err := orgContext.Selected(inv, client)
114+
userRoleStrings := make([][2]string, len(users))
115+
for index, user := range users {
116+
userAndRole := nameRoleRegex.FindStringSubmatch(user)
117+
if userAndRole == nil {
118+
return xerrors.Errorf("invalid user format %q: must match pattern 'username:role'", user)
119+
}
120+
121+
userRoleStrings[index] = [2]string{userAndRole[1], userAndRole[2]}
122+
}
123+
124+
groupRoleStrings := make([][2]string, len(groups))
125+
for index, group := range groups {
126+
groupAndRole := nameRoleRegex.FindStringSubmatch(group)
127+
if groupAndRole == nil {
128+
return xerrors.Errorf("invalid group format %q: must match pattern 'group:role'", group)
129+
}
130+
131+
groupRoleStrings[index] = [2]string{groupAndRole[1], groupAndRole[2]}
132+
}
133+
134+
userRoles, groupRoles, err := fetchUsersAndGroups(inv.Context(), fetchUsersAndGroupsParams{
135+
Client: client,
136+
OrgID: workspace.OrganizationID,
137+
OrgName: workspace.OrganizationName,
138+
Users: userRoleStrings,
139+
Groups: groupRoleStrings,
140+
DefaultRole: codersdk.WorkspaceRoleUse,
141+
})
114142
if err != nil {
115143
return err
116144
}
117145

118-
userRoles := make(map[string]codersdk.WorkspaceRole, len(users))
119-
if len(users) > 0 {
120-
orgMembers, err := client.OrganizationMembers(inv.Context(), org.ID)
121-
if err != nil {
122-
return err
123-
}
146+
err = client.UpdateWorkspaceACL(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceACL{
147+
UserRoles: userRoles,
148+
GroupRoles: groupRoles,
149+
})
150+
if err != nil {
151+
return err
152+
}
124153

125-
for _, user := range users {
126-
userAndRole := nameRoleRegex.FindStringSubmatch(user)
127-
if userAndRole == nil {
128-
return xerrors.Errorf("invalid user format %q: must match pattern 'username:role'", user)
129-
}
130-
131-
username := userAndRole[1]
132-
role := userAndRole[2]
133-
if role == "" {
134-
role = string(codersdk.WorkspaceRoleUse)
135-
}
136-
137-
userID := ""
138-
for _, member := range orgMembers {
139-
if member.Username == username {
140-
userID = member.UserID.String()
141-
break
142-
}
143-
}
144-
if userID == "" {
145-
return xerrors.Errorf("could not find user %s in the organization %s", username, org.Name)
146-
}
147-
148-
workspaceRole, err := stringToWorkspaceRole(role)
149-
if err != nil {
150-
return err
151-
}
152-
153-
userRoles[userID] = workspaceRole
154-
}
154+
acl, err := client.WorkspaceACL(inv.Context(), workspace.ID)
155+
if err != nil {
156+
return xerrors.Errorf("could not fetch current workspace ACL after sharing %w", err)
157+
}
158+
159+
out, err := workspaceACLToTable(inv.Context(), &acl)
160+
if err != nil {
161+
return err
155162
}
156163

157-
groupRoles := make(map[string]codersdk.WorkspaceRole)
158-
if len(groups) > 0 {
159-
orgGroups, err := client.Groups(inv.Context(), codersdk.GroupArguments{
160-
Organization: org.ID.String(),
161-
})
162-
if err != nil {
163-
return err
164+
_, err = fmt.Fprintln(inv.Stdout, out)
165+
return err
166+
},
167+
}
168+
169+
return cmd
170+
}
171+
172+
func (r *RootCmd) unshareWorkspace() *serpent.Command {
173+
var (
174+
client = new(codersdk.Client)
175+
users []string
176+
groups []string
177+
)
178+
179+
cmd := &serpent.Command{
180+
Use: "remove <workspace> --user <user> --group <group>",
181+
Aliases: []string{"unshare"},
182+
Short: "Remove shared access for users or groups from a workspace.",
183+
Options: serpent.OptionSet{
184+
{
185+
Name: "user",
186+
Description: "A comma separated list of users to share the workspace with.",
187+
Flag: "user",
188+
Value: serpent.StringArrayOf(&users),
189+
}, {
190+
Name: "group",
191+
Description: "A comma separated list of groups to share the workspace with.",
192+
Flag: "group",
193+
Value: serpent.StringArrayOf(&groups),
194+
},
195+
},
196+
Middleware: serpent.Chain(
197+
r.InitClient(client),
198+
serpent.RequireNArgs(1),
199+
),
200+
Handler: func(inv *serpent.Invocation) error {
201+
if len(users) == 0 && len(groups) == 0 {
202+
return xerrors.New("at least one user or group must be provided")
203+
}
204+
205+
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
206+
if err != nil {
207+
return xerrors.Errorf("could not fetch the workspace %s: %w", inv.Args[0], err)
208+
}
209+
210+
userRoleStrings := make([][2]string, len(users))
211+
for index, user := range users {
212+
if !codersdk.UsernameValidRegex.MatchString(user) {
213+
return xerrors.Errorf("invalid username")
164214
}
165215

166-
for _, group := range groups {
167-
groupAndRole := nameRoleRegex.FindStringSubmatch(group)
168-
if groupAndRole == nil {
169-
return xerrors.Errorf("invalid group format %q: must match pattern 'group:role'", group)
170-
}
171-
groupName := groupAndRole[1]
172-
role := groupAndRole[2]
173-
if role == "" {
174-
role = string(codersdk.WorkspaceRoleUse)
175-
}
176-
177-
var orgGroup *codersdk.Group
178-
for _, group := range orgGroups {
179-
if group.Name == groupName {
180-
orgGroup = &group
181-
break
182-
}
183-
}
184-
185-
if orgGroup == nil {
186-
return xerrors.Errorf("could not find group named %s belonging to the organization %s", groupName, org.Name)
187-
}
188-
189-
workspaceRole, err := stringToWorkspaceRole(role)
190-
if err != nil {
191-
return err
192-
}
193-
194-
groupRoles[orgGroup.ID.String()] = workspaceRole
216+
userRoleStrings[index] = [2]string{user, ""}
217+
}
218+
219+
groupRoleStrings := make([][2]string, len(groups))
220+
for index, group := range groups {
221+
if !codersdk.UsernameValidRegex.MatchString(group) {
222+
return xerrors.Errorf("invalid group name")
195223
}
224+
225+
groupRoleStrings[index] = [2]string{group, ""}
226+
}
227+
228+
userRoles, groupRoles, err := fetchUsersAndGroups(inv.Context(), fetchUsersAndGroupsParams{
229+
Client: client,
230+
OrgID: workspace.OrganizationID,
231+
OrgName: workspace.OrganizationName,
232+
Users: userRoleStrings,
233+
Groups: groupRoleStrings,
234+
DefaultRole: codersdk.WorkspaceRoleDeleted,
235+
})
236+
if err != nil {
237+
return err
196238
}
197239

198240
err = client.UpdateWorkspaceACL(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceACL{
@@ -227,9 +269,11 @@ func stringToWorkspaceRole(role string) (codersdk.WorkspaceRole, error) {
227269
return codersdk.WorkspaceRoleUse, nil
228270
case string(codersdk.WorkspaceRoleAdmin):
229271
return codersdk.WorkspaceRoleAdmin, nil
272+
case string(codersdk.WorkspaceRoleDeleted):
273+
return codersdk.WorkspaceRoleDeleted, nil
230274
default:
231-
return "", xerrors.Errorf("invalid role %q: expected %q or %q",
232-
role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse)
275+
return "", xerrors.Errorf("invalid role %q: expected %q, %q, or \"%q\"",
276+
role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse, codersdk.WorkspaceRoleDeleted)
233277
}
234278
}
235279

@@ -277,3 +321,96 @@ func workspaceACLToTable(ctx context.Context, acl *codersdk.WorkspaceACL) (strin
277321

278322
return out, nil
279323
}
324+
325+
type fetchUsersAndGroupsParams struct {
326+
Client *codersdk.Client
327+
OrgID uuid.UUID
328+
OrgName string
329+
Users [][2]string
330+
Groups [][2]string
331+
DefaultRole codersdk.WorkspaceRole
332+
}
333+
334+
func fetchUsersAndGroups(ctx context.Context, params fetchUsersAndGroupsParams) (userRoles map[string]codersdk.WorkspaceRole, groupRoles map[string]codersdk.WorkspaceRole, err error) {
335+
var (
336+
client = params.Client
337+
orgID = params.OrgID
338+
orgName = params.OrgName
339+
users = params.Users
340+
groups = params.Groups
341+
defaultRole = params.DefaultRole
342+
)
343+
344+
userRoles = make(map[string]codersdk.WorkspaceRole, len(users))
345+
if len(users) > 0 {
346+
orgMembers, err := client.OrganizationMembers(ctx, orgID)
347+
if err != nil {
348+
return nil, nil, err
349+
}
350+
351+
for _, user := range users {
352+
username := user[0]
353+
role := user[1]
354+
if role == "" {
355+
role = string(defaultRole)
356+
}
357+
358+
userID := ""
359+
for _, member := range orgMembers {
360+
if member.Username == username {
361+
userID = member.UserID.String()
362+
break
363+
}
364+
}
365+
if userID == "" {
366+
return nil, nil, xerrors.Errorf("could not find user %s in the organization %s", username, orgName)
367+
}
368+
369+
workspaceRole, err := stringToWorkspaceRole(role)
370+
if err != nil {
371+
return nil, nil, err
372+
}
373+
374+
userRoles[userID] = workspaceRole
375+
}
376+
}
377+
378+
groupRoles = make(map[string]codersdk.WorkspaceRole)
379+
if len(groups) > 0 {
380+
orgGroups, err := client.Groups(ctx, codersdk.GroupArguments{
381+
Organization: orgID.String(),
382+
})
383+
if err != nil {
384+
return nil, nil, err
385+
}
386+
387+
for _, group := range groups {
388+
groupName := group[0]
389+
role := group[1]
390+
if role == "" {
391+
role = string(defaultRole)
392+
}
393+
394+
var orgGroup *codersdk.Group
395+
for _, og := range orgGroups {
396+
if og.Name == groupName {
397+
orgGroup = &og
398+
break
399+
}
400+
}
401+
402+
if orgGroup == nil {
403+
return nil, nil, xerrors.Errorf("could not find group named %s belonging to the organization %s", groupName, orgName)
404+
}
405+
406+
workspaceRole, err := stringToWorkspaceRole(role)
407+
if err != nil {
408+
return nil, nil, err
409+
}
410+
411+
groupRoles[orgGroup.ID.String()] = workspaceRole
412+
}
413+
}
414+
415+
return userRoles, groupRoles, nil
416+
}

0 commit comments

Comments
 (0)