7
7
8
8
"golang.org/x/xerrors"
9
9
10
+ "github.com/google/uuid"
11
+
10
12
"github.com/coder/coder/v2/cli/cliui"
11
13
"github.com/coder/coder/v2/codersdk"
12
14
"github.com/coder/serpent"
@@ -15,8 +17,6 @@ import (
15
17
const defaultGroupDisplay = "-"
16
18
17
19
func (r * RootCmd )sharing ()* serpent.Command {
18
- orgContext := NewOrganizationContext ()
19
-
20
20
cmd := & serpent.Command {
21
21
Use :"sharing [subcommand]" ,
22
22
Short :"Commands for managing shared workspaces" ,
@@ -25,13 +25,13 @@ func (r *RootCmd) sharing() *serpent.Command {
25
25
return inv .Command .HelpHandler (inv )
26
26
},
27
27
Children : []* serpent.Command {
28
- r .shareWorkspace (orgContext ),
28
+ r .shareWorkspace (),
29
+ r .unshareWorkspace (),
29
30
r .statusWorkspaceSharing (),
30
31
},
31
32
Hidden :true ,
32
33
}
33
34
34
- orgContext .AttachOptions (cmd )
35
35
return cmd
36
36
}
37
37
@@ -70,13 +70,14 @@ func (r *RootCmd) statusWorkspaceSharing() *serpent.Command {
70
70
return cmd
71
71
}
72
72
73
- func (r * RootCmd )shareWorkspace (orgContext * OrganizationContext )* serpent.Command {
73
+ func (r * RootCmd )shareWorkspace ()* serpent.Command {
74
74
var (
75
+ client = new (codersdk.Client )
76
+ users []string
77
+ groups []string
78
+
75
79
// Username regex taken from codersdk/name.go
76
80
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
80
81
)
81
82
82
83
cmd := & serpent.Command {
@@ -110,89 +111,130 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma
110
111
return xerrors .Errorf ("could not fetch the workspace %s: %w" ,inv .Args [0 ],err )
111
112
}
112
113
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
+ })
114
142
if err != nil {
115
143
return err
116
144
}
117
145
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
+ }
124
153
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
155
162
}
156
163
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" )
164
214
}
165
215
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" )
195
223
}
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
196
238
}
197
239
198
240
err = client .UpdateWorkspaceACL (inv .Context (),workspace .ID , codersdk.UpdateWorkspaceACL {
@@ -227,9 +269,11 @@ func stringToWorkspaceRole(role string) (codersdk.WorkspaceRole, error) {
227
269
return codersdk .WorkspaceRoleUse ,nil
228
270
case string (codersdk .WorkspaceRoleAdmin ):
229
271
return codersdk .WorkspaceRoleAdmin ,nil
272
+ case string (codersdk .WorkspaceRoleDeleted ):
273
+ return codersdk .WorkspaceRoleDeleted ,nil
230
274
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 )
233
277
}
234
278
}
235
279
@@ -277,3 +321,96 @@ func workspaceACLToTable(ctx context.Context, acl *codersdk.WorkspaceACL) (strin
277
321
278
322
return out ,nil
279
323
}
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
+ }