Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit909acbc

Browse files
authored
feat: addsharing add command to the CLI (#19576)
Adds a `sharing add` command for sharing Workspaces with other users andgroups.The command allows sharing with multiple users, and groups within onecommand as well as specifying the role (`use`, or `admin`) defaulting to`use` if none is specified.In the current implementation when the command completes we show theuser the current state of the workspace ACL.```$ coder sharing add apricot-catfish-86 --user=member:admin --group=contractors:useUSER GROUP ROLEmember - adminmember contractors use```If a user is a part of multiple groups, or the workspace has beenindividually shared with them they will show up multiple times. Althoughthis is a bit confusing at first glance it's important to be able totell what the maximum role a user may have, and via what ACL they haveit.---One piece of UX to consider is that in order to be able to share aWorkspace with a user they must have a role that can read that user. Inthe tests we give the user the `ScopedRoleOrgAuditor` role.Closes[coder/internal#859](coder/internal#859)
1 parenta78d65c commit909acbc

File tree

4 files changed

+609
-0
lines changed

4 files changed

+609
-0
lines changed

‎cli/root.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
9797
r.portForward(),
9898
r.publickey(),
9999
r.resetPassword(),
100+
r.sharing(),
100101
r.state(),
101102
r.templates(),
102103
r.tokens(),

‎cli/sharing.go‎

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/v2/cli/cliui"
10+
"github.com/coder/coder/v2/codersdk"
11+
"github.com/coder/serpent"
12+
)
13+
14+
constdefaultGroupDisplay="-"
15+
16+
typeworkspaceShareRowstruct {
17+
Userstring`table:"user"`
18+
Groupstring`table:"group,default_sort"`
19+
Role codersdk.WorkspaceRole`table:"role"`
20+
}
21+
22+
func (r*RootCmd)sharing()*serpent.Command {
23+
orgContext:=NewOrganizationContext()
24+
25+
cmd:=&serpent.Command{
26+
Use:"sharing [subcommand]",
27+
Short:"Commands for managing shared workspaces",
28+
Aliases: []string{"share"},
29+
Handler:func(inv*serpent.Invocation)error {
30+
returninv.Command.HelpHandler(inv)
31+
},
32+
Children: []*serpent.Command{r.shareWorkspace(orgContext)},
33+
Hidden:true,
34+
}
35+
36+
orgContext.AttachOptions(cmd)
37+
returncmd
38+
}
39+
40+
func (r*RootCmd)shareWorkspace(orgContext*OrganizationContext)*serpent.Command {
41+
var (
42+
// Username regex taken from codersdk/name.go
43+
nameRoleRegex=regexp.MustCompile(`(^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)+(?::([A-Za-z0-9-]+))?`)
44+
client=new(codersdk.Client)
45+
users []string
46+
groups []string
47+
formatter=cliui.NewOutputFormatter(
48+
cliui.TableFormat(
49+
[]workspaceShareRow{}, []string{"User","Group","Role"}),
50+
cliui.JSONFormat(),
51+
)
52+
)
53+
54+
cmd:=&serpent.Command{
55+
Use:"add <workspace> --user <user>:<role> --group <group>:<role>",
56+
Aliases: []string{"share"},
57+
Short:"Share a workspace with a user or group.",
58+
Options: serpent.OptionSet{
59+
{
60+
Name:"user",
61+
Description:"A comma separated list of users to share the workspace with.",
62+
Flag:"user",
63+
Value:serpent.StringArrayOf(&users),
64+
}, {
65+
Name:"group",
66+
Description:"A comma separated list of groups to share the workspace with.",
67+
Flag:"group",
68+
Value:serpent.StringArrayOf(&groups),
69+
},
70+
},
71+
Middleware:serpent.Chain(
72+
r.InitClient(client),
73+
serpent.RequireNArgs(1),
74+
),
75+
Handler:func(inv*serpent.Invocation)error {
76+
iflen(users)==0&&len(groups)==0 {
77+
returnxerrors.New("at least one user or group must be provided")
78+
}
79+
80+
workspace,err:=namedWorkspace(inv.Context(),client,inv.Args[0])
81+
iferr!=nil {
82+
returnxerrors.Errorf("could not fetch the workspace %s: %w",inv.Args[0],err)
83+
}
84+
85+
org,err:=orgContext.Selected(inv,client)
86+
iferr!=nil {
87+
returnerr
88+
}
89+
90+
userRoles:=make(map[string]codersdk.WorkspaceRole,len(users))
91+
iflen(users)>0 {
92+
orgMembers,err:=client.OrganizationMembers(inv.Context(),org.ID)
93+
iferr!=nil {
94+
returnerr
95+
}
96+
97+
for_,user:=rangeusers {
98+
userAndRole:=nameRoleRegex.FindStringSubmatch(user)
99+
ifuserAndRole==nil {
100+
returnxerrors.Errorf("invalid user format %q: must match pattern 'username:role'",user)
101+
}
102+
103+
username:=userAndRole[1]
104+
role:=userAndRole[2]
105+
ifrole=="" {
106+
role=string(codersdk.WorkspaceRoleUse)
107+
}
108+
109+
userID:=""
110+
for_,member:=rangeorgMembers {
111+
ifmember.Username==username {
112+
userID=member.UserID.String()
113+
break
114+
}
115+
}
116+
ifuserID=="" {
117+
returnxerrors.Errorf("could not find user %s in the organization %s",username,org.Name)
118+
}
119+
120+
workspaceRole,err:=stringToWorkspaceRole(role)
121+
iferr!=nil {
122+
returnerr
123+
}
124+
125+
userRoles[userID]=workspaceRole
126+
}
127+
}
128+
129+
groupRoles:=make(map[string]codersdk.WorkspaceRole)
130+
iflen(groups)>0 {
131+
orgGroups,err:=client.Groups(inv.Context(), codersdk.GroupArguments{
132+
Organization:org.ID.String(),
133+
})
134+
iferr!=nil {
135+
returnerr
136+
}
137+
138+
for_,group:=rangegroups {
139+
groupAndRole:=nameRoleRegex.FindStringSubmatch(group)
140+
ifgroupAndRole==nil {
141+
returnxerrors.Errorf("invalid group format %q: must match pattern 'group:role'",group)
142+
}
143+
groupName:=groupAndRole[1]
144+
role:=groupAndRole[2]
145+
ifrole=="" {
146+
role=string(codersdk.WorkspaceRoleUse)
147+
}
148+
149+
varorgGroup*codersdk.Group
150+
for_,group:=rangeorgGroups {
151+
ifgroup.Name==groupName {
152+
orgGroup=&group
153+
break
154+
}
155+
}
156+
157+
iforgGroup==nil {
158+
returnxerrors.Errorf("could not find group named %s belonging to the organization %s",groupName,org.Name)
159+
}
160+
161+
workspaceRole,err:=stringToWorkspaceRole(role)
162+
iferr!=nil {
163+
returnerr
164+
}
165+
166+
groupRoles[orgGroup.ID.String()]=workspaceRole
167+
}
168+
}
169+
170+
err=client.UpdateWorkspaceACL(inv.Context(),workspace.ID, codersdk.UpdateWorkspaceACL{
171+
UserRoles:userRoles,
172+
GroupRoles:groupRoles,
173+
})
174+
iferr!=nil {
175+
returnerr
176+
}
177+
178+
workspaceACL,err:=client.WorkspaceACL(inv.Context(),workspace.ID)
179+
iferr!=nil {
180+
returnxerrors.Errorf("could not fetch current workspace ACL after sharing %w",err)
181+
}
182+
183+
outputRows:=make([]workspaceShareRow,0)
184+
for_,user:=rangeworkspaceACL.Users {
185+
ifuser.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:=rangeworkspaceACL.Groups {
196+
ifgroup.Role==codersdk.WorkspaceRoleDeleted {
197+
continue
198+
}
199+
200+
for_,user:=rangegroup.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)
209+
iferr!=nil {
210+
returnerr
211+
}
212+
213+
_,err=fmt.Fprintln(inv.Stdout,out)
214+
returnerr
215+
},
216+
}
217+
218+
returncmd
219+
}
220+
221+
funcstringToWorkspaceRole(rolestring) (codersdk.WorkspaceRole,error) {
222+
switchrole {
223+
casestring(codersdk.WorkspaceRoleUse):
224+
returncodersdk.WorkspaceRoleUse,nil
225+
casestring(codersdk.WorkspaceRoleAdmin):
226+
returncodersdk.WorkspaceRoleAdmin,nil
227+
default:
228+
return"",xerrors.Errorf("invalid role %q: expected %q or %q",
229+
role,codersdk.WorkspaceRoleAdmin,codersdk.WorkspaceRoleUse)
230+
}
231+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp