|
| 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 | +} |