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

Commit3de71bd

Browse files
committed
feat: add cli command to edit custom roles
1 parentbcf6263 commit3de71bd

File tree

4 files changed

+275
-8
lines changed

4 files changed

+275
-8
lines changed

‎cli/cliui/parameter.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
4343
return"",err
4444
}
4545

46-
values,err:=MultiSelect(inv,options)
46+
values,err:=MultiSelect(inv,MultiSelectOptions{
47+
Options:options,
48+
Defaults:options,
49+
})
4750
iferr==nil {
4851
v,err:=json.Marshal(&values)
4952
iferr!=nil {

‎cli/cliui/select.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func init() {
2121
{{- .CurrentOpt.Value}}
2222
{{- color "reset"}}
2323
{{end}}
24-
24+
{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }}
2525
{{- if not .ShowAnswer }}
2626
{{- if .Config.Icons.Help.Text }}
2727
{{- if .FilterMessage }}{{ "Search:" }}{{ .FilterMessage }}
@@ -44,18 +44,20 @@ func init() {
4444
{{- " "}}{{- .CurrentOpt.Value}}
4545
{{end}}
4646
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
47+
{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }}
4748
{{- if not .ShowAnswer }}
4849
{{- "\n"}}
4950
{{- range $ix, $option := .PageEntries}}
5051
{{- template "option" $.IterateOption $ix $option}}
5152
{{- end}}
52-
{{- end}}`
53+
{{- end}}`
5354
}
5455

5556
typeSelectOptionsstruct {
5657
Options []string
5758
// Default will be highlighted first if it's a valid option.
5859
Defaultstring
60+
Messagestring
5961
Sizeint
6062
HideSearchbool
6163
}
@@ -122,6 +124,7 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
122124
Options:opts.Options,
123125
Default:defaultOption,
124126
PageSize:opts.Size,
127+
Message:opts.Message,
125128
},&value,survey.WithIcons(func(is*survey.IconSet) {
126129
is.Help.Text="Type to search"
127130
ifopts.HideSearch {
@@ -138,15 +141,22 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
138141
returnvalue,err
139142
}
140143

141-
funcMultiSelect(inv*serpent.Invocation,items []string) ([]string,error) {
144+
typeMultiSelectOptionsstruct {
145+
Messagestring
146+
Options []string
147+
Defaults []string
148+
}
149+
150+
funcMultiSelect(inv*serpent.Invocation,optsMultiSelectOptions) ([]string,error) {
142151
// Similar hack is applied to Select()
143152
ifflag.Lookup("test.v")!=nil {
144-
returnitems,nil
153+
returnopts.Defaults,nil
145154
}
146155

147156
prompt:=&survey.MultiSelect{
148-
Options:items,
149-
Default:items,
157+
Message:opts.Message,
158+
Options:opts.Options,
159+
Default:opts.Defaults,
150160
}
151161

152162
varvalues []string

‎cli/cliui/select_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
107107
varvalues []string
108108
cmd:=&serpent.Command{
109109
Handler:func(inv*serpent.Invocation)error {
110-
selectedItems,err:=cliui.MultiSelect(inv,items)
110+
selectedItems,err:=cliui.MultiSelect(inv, cliui.MultiSelectOptions{
111+
Options:items,
112+
Defaults:items,
113+
})
111114
iferr==nil {
112115
values=selectedItems
113116
}

‎enterprise/cli/roleedit.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strings"
9+
10+
"golang.org/x/xerrors"
11+
12+
"github.com/coder/coder/v2/cli"
13+
"github.com/coder/coder/v2/cli/cliui"
14+
"github.com/coder/coder/v2/coderd/util/slice"
15+
"github.com/coder/coder/v2/codersdk"
16+
"github.com/coder/serpent"
17+
)
18+
19+
func (r*RootCmd)editRole()*serpent.Command {
20+
formatter:=cliui.NewOutputFormatter(
21+
cliui.ChangeFormatterData(
22+
cliui.TableFormat([]codersdk.Role{}, []string{"name","display_name","site_permissions","org_permissions","user_permissions"}),
23+
func(dataany) (any,error) {
24+
return []codersdk.Role{data.(codersdk.Role)},nil
25+
},
26+
),
27+
cliui.JSONFormat(),
28+
)
29+
30+
var (
31+
dryRunbool
32+
)
33+
34+
client:=new(codersdk.Client)
35+
cmd:=&serpent.Command{
36+
Use:"edit <role_name>",
37+
Short:"Edit a custom role",
38+
Long:cli.FormatExamples(
39+
cli.Example{
40+
Description:"Run with an input.json file",
41+
Command:"coder roles edit custom_name < role.json",
42+
},
43+
),
44+
Options: []serpent.Option{
45+
cliui.SkipPromptOption(),
46+
{
47+
Name:"dry-run",
48+
Description:"Does all the work, but does not submit the final updated role.",
49+
Flag:"dry-run",
50+
Value:serpent.BoolOf(&dryRun),
51+
},
52+
},
53+
Middleware:serpent.Chain(
54+
serpent.RequireNArgs(1),
55+
r.InitClient(client),
56+
),
57+
Handler:func(inv*serpent.Invocation)error {
58+
ctx:=inv.Context()
59+
roles,err:=client.ListSiteRoles(ctx)
60+
iferr!=nil {
61+
returnxerrors.Errorf("listing roles: %w",err)
62+
}
63+
64+
// Make sure the role actually exists first
65+
varoriginalRole codersdk.AssignableRoles
66+
for_,r:=rangeroles {
67+
ifstrings.EqualFold(inv.Args[0],r.Name) {
68+
originalRole=r
69+
break
70+
}
71+
}
72+
73+
iforiginalRole.Name=="" {
74+
_,err=cliui.Prompt(inv, cliui.PromptOptions{
75+
Text:"No role exists with that name, do you want to create one?",
76+
Default:"yes",
77+
IsConfirm:true,
78+
})
79+
iferr!=nil {
80+
returnxerrors.Errorf("abort: %w",err)
81+
}
82+
83+
originalRole.Role= codersdk.Role{
84+
Name:inv.Args[0],
85+
}
86+
}
87+
88+
varcustomRole*codersdk.Role
89+
// Either interactive, or take input mode.
90+
fi,_:=os.Stdin.Stat()
91+
if (fi.Mode()&os.ModeCharDevice)==0 {
92+
bytes,err:=io.ReadAll(os.Stdin)
93+
iferr!=nil {
94+
returnxerrors.Errorf("reading stdin: %w",err)
95+
}
96+
97+
err=json.Unmarshal(bytes,customRole)
98+
iferr!=nil {
99+
returnxerrors.Errorf("parsing stdin json: %w",err)
100+
}
101+
}else {
102+
// Interactive mode
103+
iflen(originalRole.OrganizationPermissions)>0 {
104+
returnxerrors.Errorf("unable to edit role in interactive mode, it contains organization permissions")
105+
}
106+
107+
iflen(originalRole.UserPermissions)>0 {
108+
returnxerrors.Errorf("unable to edit role in interactive mode, it contains user permissions")
109+
}
110+
111+
customRole,err=interactiveEdit(inv,&originalRole.Role)
112+
iferr!=nil {
113+
returnxerrors.Errorf("editing role: %w",err)
114+
}
115+
}
116+
117+
totalOrg:=0
118+
for_,o:=rangecustomRole.OrganizationPermissions {
119+
totalOrg+=len(o)
120+
}
121+
preview:=fmt.Sprintf("perms: %d site, %d over %d orgs, %d user",
122+
len(customRole.SitePermissions),totalOrg,len(customRole.OrganizationPermissions),len(customRole.UserPermissions))
123+
_,err=cliui.Prompt(inv, cliui.PromptOptions{
124+
Text:"Are you sure you wish to update the role? "+preview,
125+
Default:"yes",
126+
IsConfirm:true,
127+
})
128+
iferr!=nil {
129+
returnxerrors.Errorf("abort: %w",err)
130+
}
131+
132+
varupdated codersdk.Role
133+
ifdryRun {
134+
// Do not actually post
135+
updated=*customRole
136+
}else {
137+
updated,err=client.PatchRole(ctx,*customRole)
138+
iferr!=nil {
139+
returnfmt.Errorf("patch role: %w",err)
140+
}
141+
}
142+
143+
_,err=formatter.Format(ctx,updated)
144+
iferr!=nil {
145+
returnxerrors.Errorf("formatting: %w",err)
146+
}
147+
returnnil
148+
},
149+
}
150+
151+
formatter.AttachOptions(&cmd.Options)
152+
returncmd
153+
}
154+
155+
funcinteractiveEdit(inv*serpent.Invocation,role*codersdk.Role) (*codersdk.Role,error) {
156+
allowedResources:= []codersdk.RBACResource{
157+
codersdk.ResourceTemplate,
158+
codersdk.ResourceWorkspace,
159+
codersdk.ResourceUser,
160+
codersdk.ResourceGroup,
161+
}
162+
163+
constdone="Finish and submit changes"
164+
constabort="Cancel changes"
165+
166+
// Now starts the role editing "game".
167+
customRoleLoop:
168+
for {
169+
selected,err:=cliui.Select(inv, cliui.SelectOptions{
170+
Message:"Select which resources to edit permissions",
171+
Options:append(permissionPreviews(role,allowedResources),done,abort),
172+
})
173+
iferr!=nil {
174+
returnrole,xerrors.Errorf("selecting resource: %w",err)
175+
}
176+
switchselected {
177+
casedone:
178+
break customRoleLoop
179+
caseabort:
180+
returnrole,xerrors.Errorf("edit role %q aborted",role.Name)
181+
default:
182+
strs:=strings.Split(selected,"::")
183+
resource:=strings.TrimSpace(strs[0])
184+
185+
actions,err:=cliui.MultiSelect(inv, cliui.MultiSelectOptions{
186+
Message:fmt.Sprintf("Select actions to allow across the whole deployment for resources=%q",resource),
187+
Options:slice.ToStrings(codersdk.RBACResourceActions[codersdk.RBACResource(resource)]),
188+
Defaults:defaultActions(role,resource),
189+
})
190+
iferr!=nil {
191+
returnrole,xerrors.Errorf("selecting actions for resource %q: %w",resource,err)
192+
}
193+
applyResourceActions(role,resource,actions)
194+
// back to resources!
195+
}
196+
}
197+
// This println is required because the prompt ends us on the same line as some text.
198+
fmt.Println()
199+
200+
returnrole,nil
201+
}
202+
203+
funcapplyResourceActions(role*codersdk.Role,resourcestring,actions []string) {
204+
// Construct new site perms with only new perms for the resource
205+
keep:=make([]codersdk.Permission,0)
206+
for_,perm:=rangerole.SitePermissions {
207+
perm:=perm
208+
ifstring(perm.ResourceType)!=resource {
209+
keep=append(keep,perm)
210+
}
211+
}
212+
213+
// Add new perms
214+
for_,action:=rangeactions {
215+
keep=append(keep, codersdk.Permission{
216+
Negate:false,
217+
ResourceType:codersdk.RBACResource(resource),
218+
Action:codersdk.RBACAction(action),
219+
})
220+
}
221+
222+
role.SitePermissions=keep
223+
}
224+
225+
funcdefaultActions(role*codersdk.Role,resourcestring) []string {
226+
defaults:=make([]string,0)
227+
for_,perm:=rangerole.SitePermissions {
228+
ifstring(perm.ResourceType)==resource {
229+
defaults=append(defaults,string(perm.Action))
230+
}
231+
}
232+
returndefaults
233+
}
234+
235+
funcpermissionPreviews(role*codersdk.Role,resources []codersdk.RBACResource) []string {
236+
previews:=make([]string,0,len(resources))
237+
for_,resource:=rangeresources {
238+
previews=append(previews,permissionPreview(role,resource))
239+
}
240+
returnpreviews
241+
}
242+
243+
funcpermissionPreview(role*codersdk.Role,resource codersdk.RBACResource)string {
244+
count:=0
245+
for_,perm:=rangerole.SitePermissions {
246+
ifperm.ResourceType==resource {
247+
count++
248+
}
249+
}
250+
returnfmt.Sprintf("%s :: %d permissions",resource,count)
251+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp