mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat: split cli roles edit command into create and update commands (#17121)
Closes #14239
This commit is contained in:
@ -26,7 +26,8 @@ func (r *RootCmd) organizationRoles(orgContext *OrganizationContext) *serpent.Co
|
|||||||
},
|
},
|
||||||
Children: []*serpent.Command{
|
Children: []*serpent.Command{
|
||||||
r.showOrganizationRoles(orgContext),
|
r.showOrganizationRoles(orgContext),
|
||||||
r.editOrganizationRole(orgContext),
|
r.updateOrganizationRole(orgContext),
|
||||||
|
r.createOrganizationRole(orgContext),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
@ -99,7 +100,7 @@ func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpen
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent.Command {
|
func (r *RootCmd) createOrganizationRole(orgContext *OrganizationContext) *serpent.Command {
|
||||||
formatter := cliui.NewOutputFormatter(
|
formatter := cliui.NewOutputFormatter(
|
||||||
cliui.ChangeFormatterData(
|
cliui.ChangeFormatterData(
|
||||||
cliui.TableFormat([]roleTableRow{}, []string{"name", "display name", "site permissions", "organization permissions", "user permissions"}),
|
cliui.TableFormat([]roleTableRow{}, []string{"name", "display name", "site permissions", "organization permissions", "user permissions"}),
|
||||||
@ -118,12 +119,12 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
|||||||
|
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
cmd := &serpent.Command{
|
cmd := &serpent.Command{
|
||||||
Use: "edit <role_name>",
|
Use: "create <role_name>",
|
||||||
Short: "Edit an organization custom role",
|
Short: "Create a new organization custom role",
|
||||||
Long: FormatExamples(
|
Long: FormatExamples(
|
||||||
Example{
|
Example{
|
||||||
Description: "Run with an input.json file",
|
Description: "Run with an input.json file",
|
||||||
Command: "coder roles edit --stdin < role.json",
|
Command: "coder organization -O <organization_name> roles create --stidin < role.json",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Options: []serpent.Option{
|
Options: []serpent.Option{
|
||||||
@ -152,10 +153,13 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewRole := true
|
existingRoles, err := client.ListOrganizationRoles(ctx, org.ID)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("listing existing roles: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var customRole codersdk.Role
|
var customRole codersdk.Role
|
||||||
if jsonInput {
|
if jsonInput {
|
||||||
// JSON Upload mode
|
|
||||||
bytes, err := io.ReadAll(inv.Stdin)
|
bytes, err := io.ReadAll(inv.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("reading stdin: %w", err)
|
return xerrors.Errorf("reading stdin: %w", err)
|
||||||
@ -175,29 +179,148 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
|||||||
return xerrors.Errorf("json input does not appear to be a valid role")
|
return xerrors.Errorf("json input does not appear to be a valid role")
|
||||||
}
|
}
|
||||||
|
|
||||||
existingRoles, err := client.ListOrganizationRoles(ctx, org.ID)
|
if role := existingRole(customRole.Name, existingRoles); role != nil {
|
||||||
if err != nil {
|
return xerrors.Errorf("The role %s already exists. If you'd like to edit this role use the update command instead", customRole.Name)
|
||||||
return xerrors.Errorf("listing existing roles: %w", err)
|
|
||||||
}
|
}
|
||||||
for _, existingRole := range existingRoles {
|
} else {
|
||||||
if strings.EqualFold(customRole.Name, existingRole.Name) {
|
if len(inv.Args) == 0 {
|
||||||
// Editing an existing role
|
return xerrors.Errorf("missing role name argument, usage: \"coder organizations roles create <role_name>\"")
|
||||||
createNewRole = false
|
}
|
||||||
break
|
|
||||||
|
if role := existingRole(inv.Args[0], existingRoles); role != nil {
|
||||||
|
return xerrors.Errorf("The role %s already exists. If you'd like to edit this role use the update command instead", inv.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
interactiveRole, err := interactiveOrgRoleEdit(inv, org.ID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("editing role: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
customRole = *interactiveRole
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated codersdk.Role
|
||||||
|
if dryRun {
|
||||||
|
// Do not actually post
|
||||||
|
updated = customRole
|
||||||
|
} else {
|
||||||
|
updated, err = client.CreateOrganizationRole(ctx, customRole)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("patch role: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := formatter.Format(ctx, updated)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("formatting: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintln(inv.Stdout, output)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootCmd) updateOrganizationRole(orgContext *OrganizationContext) *serpent.Command {
|
||||||
|
formatter := cliui.NewOutputFormatter(
|
||||||
|
cliui.ChangeFormatterData(
|
||||||
|
cliui.TableFormat([]roleTableRow{}, []string{"name", "display name", "site permissions", "organization permissions", "user permissions"}),
|
||||||
|
func(data any) (any, error) {
|
||||||
|
typed, _ := data.(codersdk.Role)
|
||||||
|
return []roleTableRow{roleToTableView(typed)}, nil
|
||||||
|
},
|
||||||
|
),
|
||||||
|
cliui.JSONFormat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dryRun bool
|
||||||
|
jsonInput bool
|
||||||
|
)
|
||||||
|
|
||||||
|
client := new(codersdk.Client)
|
||||||
|
cmd := &serpent.Command{
|
||||||
|
Use: "update <role_name>",
|
||||||
|
Short: "Update an organization custom role",
|
||||||
|
Long: FormatExamples(
|
||||||
|
Example{
|
||||||
|
Description: "Run with an input.json file",
|
||||||
|
Command: "coder roles update --stdin < role.json",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Options: []serpent.Option{
|
||||||
|
cliui.SkipPromptOption(),
|
||||||
|
{
|
||||||
|
Name: "dry-run",
|
||||||
|
Description: "Does all the work, but does not submit the final updated role.",
|
||||||
|
Flag: "dry-run",
|
||||||
|
Value: serpent.BoolOf(&dryRun),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "stdin",
|
||||||
|
Description: "Reads stdin for the json role definition to upload.",
|
||||||
|
Flag: "stdin",
|
||||||
|
Value: serpent.BoolOf(&jsonInput),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Middleware: serpent.Chain(
|
||||||
|
serpent.RequireRangeArgs(0, 1),
|
||||||
|
r.InitClient(client),
|
||||||
|
),
|
||||||
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
|
ctx := inv.Context()
|
||||||
|
org, err := orgContext.Selected(inv, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
existingRoles, err := client.ListOrganizationRoles(ctx, org.ID)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("listing existing roles: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var customRole codersdk.Role
|
||||||
|
if jsonInput {
|
||||||
|
bytes, err := io.ReadAll(inv.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("reading stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(bytes, &customRole)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("parsing stdin json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if customRole.Name == "" {
|
||||||
|
arr := make([]json.RawMessage, 0)
|
||||||
|
err = json.Unmarshal(bytes, &arr)
|
||||||
|
if err == nil && len(arr) > 0 {
|
||||||
|
return xerrors.Errorf("only 1 role can be sent at a time")
|
||||||
}
|
}
|
||||||
|
return xerrors.Errorf("json input does not appear to be a valid role")
|
||||||
|
}
|
||||||
|
|
||||||
|
if role := existingRole(customRole.Name, existingRoles); role == nil {
|
||||||
|
return xerrors.Errorf("The role %s does not exist. If you'd like to create this role use the create command instead", customRole.Name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if len(inv.Args) == 0 {
|
if len(inv.Args) == 0 {
|
||||||
return xerrors.Errorf("missing role name argument, usage: \"coder organizations roles edit <role_name>\"")
|
return xerrors.Errorf("missing role name argument, usage: \"coder organizations roles edit <role_name>\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
interactiveRole, newRole, err := interactiveOrgRoleEdit(inv, org.ID, client)
|
role := existingRole(inv.Args[0], existingRoles)
|
||||||
|
if role == nil {
|
||||||
|
return xerrors.Errorf("The role %s does not exist. If you'd like to create this role use the create command instead", inv.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
interactiveRole, err := interactiveOrgRoleEdit(inv, org.ID, &role.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("editing role: %w", err)
|
return xerrors.Errorf("editing role: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
customRole = *interactiveRole
|
customRole = *interactiveRole
|
||||||
createNewRole = newRole
|
|
||||||
|
|
||||||
preview := fmt.Sprintf("permissions: %d site, %d org, %d user",
|
preview := fmt.Sprintf("permissions: %d site, %d org, %d user",
|
||||||
len(customRole.SitePermissions), len(customRole.OrganizationPermissions), len(customRole.UserPermissions))
|
len(customRole.SitePermissions), len(customRole.OrganizationPermissions), len(customRole.UserPermissions))
|
||||||
@ -216,12 +339,7 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
|||||||
// Do not actually post
|
// Do not actually post
|
||||||
updated = customRole
|
updated = customRole
|
||||||
} else {
|
} else {
|
||||||
switch createNewRole {
|
updated, err = client.UpdateOrganizationRole(ctx, customRole)
|
||||||
case true:
|
|
||||||
updated, err = client.CreateOrganizationRole(ctx, customRole)
|
|
||||||
default:
|
|
||||||
updated, err = client.UpdateOrganizationRole(ctx, customRole)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("patch role: %w", err)
|
return xerrors.Errorf("patch role: %w", err)
|
||||||
}
|
}
|
||||||
@ -241,50 +359,27 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func interactiveOrgRoleEdit(inv *serpent.Invocation, orgID uuid.UUID, client *codersdk.Client) (*codersdk.Role, bool, error) {
|
func interactiveOrgRoleEdit(inv *serpent.Invocation, orgID uuid.UUID, updateRole *codersdk.Role) (*codersdk.Role, error) {
|
||||||
newRole := false
|
var originalRole codersdk.Role
|
||||||
ctx := inv.Context()
|
if updateRole == nil {
|
||||||
roles, err := client.ListOrganizationRoles(ctx, orgID)
|
originalRole = codersdk.Role{
|
||||||
if err != nil {
|
|
||||||
return nil, newRole, xerrors.Errorf("listing roles: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the role actually exists first
|
|
||||||
var originalRole codersdk.AssignableRoles
|
|
||||||
for _, r := range roles {
|
|
||||||
if strings.EqualFold(inv.Args[0], r.Name) {
|
|
||||||
originalRole = r
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if originalRole.Name == "" {
|
|
||||||
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
||||||
Text: "No organization role exists with that name, do you want to create one?",
|
|
||||||
Default: "yes",
|
|
||||||
IsConfirm: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, newRole, xerrors.Errorf("abort: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
originalRole.Role = codersdk.Role{
|
|
||||||
Name: inv.Args[0],
|
Name: inv.Args[0],
|
||||||
OrganizationID: orgID.String(),
|
OrganizationID: orgID.String(),
|
||||||
}
|
}
|
||||||
newRole = true
|
} else {
|
||||||
|
originalRole = *updateRole
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some checks since interactive mode is limited in what it currently sees
|
// Some checks since interactive mode is limited in what it currently sees
|
||||||
if len(originalRole.SitePermissions) > 0 {
|
if len(originalRole.SitePermissions) > 0 {
|
||||||
return nil, newRole, xerrors.Errorf("unable to edit role in interactive mode, it contains site wide permissions")
|
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains site wide permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(originalRole.UserPermissions) > 0 {
|
if len(originalRole.UserPermissions) > 0 {
|
||||||
return nil, newRole, xerrors.Errorf("unable to edit role in interactive mode, it contains user permissions")
|
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains user permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
role := &originalRole.Role
|
role := &originalRole
|
||||||
allowedResources := []codersdk.RBACResource{
|
allowedResources := []codersdk.RBACResource{
|
||||||
codersdk.ResourceTemplate,
|
codersdk.ResourceTemplate,
|
||||||
codersdk.ResourceWorkspace,
|
codersdk.ResourceWorkspace,
|
||||||
@ -303,13 +398,13 @@ customRoleLoop:
|
|||||||
Options: append(permissionPreviews(role, allowedResources), done, abort),
|
Options: append(permissionPreviews(role, allowedResources), done, abort),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return role, newRole, xerrors.Errorf("selecting resource: %w", err)
|
return role, xerrors.Errorf("selecting resource: %w", err)
|
||||||
}
|
}
|
||||||
switch selected {
|
switch selected {
|
||||||
case done:
|
case done:
|
||||||
break customRoleLoop
|
break customRoleLoop
|
||||||
case abort:
|
case abort:
|
||||||
return role, newRole, xerrors.Errorf("edit role %q aborted", role.Name)
|
return role, xerrors.Errorf("edit role %q aborted", role.Name)
|
||||||
default:
|
default:
|
||||||
strs := strings.Split(selected, "::")
|
strs := strings.Split(selected, "::")
|
||||||
resource := strings.TrimSpace(strs[0])
|
resource := strings.TrimSpace(strs[0])
|
||||||
@ -320,7 +415,7 @@ customRoleLoop:
|
|||||||
Defaults: defaultActions(role, resource),
|
Defaults: defaultActions(role, resource),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return role, newRole, xerrors.Errorf("selecting actions for resource %q: %w", resource, err)
|
return role, xerrors.Errorf("selecting actions for resource %q: %w", resource, err)
|
||||||
}
|
}
|
||||||
applyOrgResourceActions(role, resource, actions)
|
applyOrgResourceActions(role, resource, actions)
|
||||||
// back to resources!
|
// back to resources!
|
||||||
@ -329,7 +424,7 @@ customRoleLoop:
|
|||||||
// This println is required because the prompt ends us on the same line as some text.
|
// This println is required because the prompt ends us on the same line as some text.
|
||||||
_, _ = fmt.Println()
|
_, _ = fmt.Println()
|
||||||
|
|
||||||
return role, newRole, nil
|
return role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyOrgResourceActions(role *codersdk.Role, resource string, actions []string) {
|
func applyOrgResourceActions(role *codersdk.Role, resource string, actions []string) {
|
||||||
@ -405,6 +500,16 @@ func roleToTableView(role codersdk.Role) roleTableRow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func existingRole(newRoleName string, existingRoles []codersdk.AssignableRoles) *codersdk.AssignableRoles {
|
||||||
|
for _, existingRole := range existingRoles {
|
||||||
|
if strings.EqualFold(newRoleName, existingRole.Name) {
|
||||||
|
return &existingRole
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type roleTableRow struct {
|
type roleTableRow struct {
|
||||||
Name string `table:"name,default_sort"`
|
Name string `table:"name,default_sort"`
|
||||||
DisplayName string `table:"display name"`
|
DisplayName string `table:"display name"`
|
||||||
|
@ -8,8 +8,9 @@ USAGE:
|
|||||||
Aliases: role
|
Aliases: role
|
||||||
|
|
||||||
SUBCOMMANDS:
|
SUBCOMMANDS:
|
||||||
edit Edit an organization custom role
|
create Create a new organization custom role
|
||||||
show Show role(s)
|
show Show role(s)
|
||||||
|
update Update an organization custom role
|
||||||
|
|
||||||
———
|
———
|
||||||
Run `coder --help` for a list of global options.
|
Run `coder --help` for a list of global options.
|
||||||
|
24
cli/testdata/coder_organizations_roles_create_--help.golden
vendored
Normal file
24
cli/testdata/coder_organizations_roles_create_--help.golden
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
coder v0.0.0-devel
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
coder organizations roles create [flags] <role_name>
|
||||||
|
|
||||||
|
Create a new organization custom role
|
||||||
|
|
||||||
|
- Run with an input.json file:
|
||||||
|
|
||||||
|
$ coder organization -O <organization_name> roles create --stidin <
|
||||||
|
role.json
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--dry-run bool
|
||||||
|
Does all the work, but does not submit the final updated role.
|
||||||
|
|
||||||
|
--stdin bool
|
||||||
|
Reads stdin for the json role definition to upload.
|
||||||
|
|
||||||
|
-y, --yes bool
|
||||||
|
Bypass prompts.
|
||||||
|
|
||||||
|
———
|
||||||
|
Run `coder --help` for a list of global options.
|
@ -1,13 +1,13 @@
|
|||||||
coder v0.0.0-devel
|
coder v0.0.0-devel
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
coder organizations roles edit [flags] <role_name>
|
coder organizations roles update [flags] <role_name>
|
||||||
|
|
||||||
Edit an organization custom role
|
Update an organization custom role
|
||||||
|
|
||||||
- Run with an input.json file:
|
- Run with an input.json file:
|
||||||
|
|
||||||
$ coder roles edit --stdin < role.json
|
$ coder roles update --stdin < role.json
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-c, --column [name|display name|organization id|site permissions|organization permissions|user permissions] (default: name,display name,site permissions,organization permissions,user permissions)
|
-c, --column [name|display name|organization id|site permissions|organization permissions|user permissions] (default: name,display name,site permissions,organization permissions,user permissions)
|
@ -1200,15 +1200,20 @@
|
|||||||
"path": "reference/cli/organizations_roles.md"
|
"path": "reference/cli/organizations_roles.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "organizations roles edit",
|
"title": "organizations roles create",
|
||||||
"description": "Edit an organization custom role",
|
"description": "Create a new organization custom role",
|
||||||
"path": "reference/cli/organizations_roles_edit.md"
|
"path": "reference/cli/organizations_roles_create.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "organizations roles show",
|
"title": "organizations roles show",
|
||||||
"description": "Show role(s)",
|
"description": "Show role(s)",
|
||||||
"path": "reference/cli/organizations_roles_show.md"
|
"path": "reference/cli/organizations_roles_show.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "organizations roles update",
|
||||||
|
"description": "Update an organization custom role",
|
||||||
|
"path": "reference/cli/organizations_roles_update.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "organizations settings",
|
"title": "organizations settings",
|
||||||
"description": "Manage organization settings.",
|
"description": "Manage organization settings.",
|
||||||
|
9
docs/reference/cli/organizations_roles.md
generated
9
docs/reference/cli/organizations_roles.md
generated
@ -15,7 +15,8 @@ coder organizations roles
|
|||||||
|
|
||||||
## Subcommands
|
## Subcommands
|
||||||
|
|
||||||
| Name | Purpose |
|
| Name | Purpose |
|
||||||
|----------------------------------------------------|----------------------------------|
|
|--------------------------------------------------------|---------------------------------------|
|
||||||
| [<code>show</code>](./organizations_roles_show.md) | Show role(s) |
|
| [<code>show</code>](./organizations_roles_show.md) | Show role(s) |
|
||||||
| [<code>edit</code>](./organizations_roles_edit.md) | Edit an organization custom role |
|
| [<code>update</code>](./organizations_roles_update.md) | Update an organization custom role |
|
||||||
|
| [<code>create</code>](./organizations_roles_create.md) | Create a new organization custom role |
|
||||||
|
44
docs/reference/cli/organizations_roles_create.md
generated
Normal file
44
docs/reference/cli/organizations_roles_create.md
generated
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||||
|
# organizations roles create
|
||||||
|
|
||||||
|
Create a new organization custom role
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```console
|
||||||
|
coder organizations roles create [flags] <role_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
```console
|
||||||
|
- Run with an input.json file:
|
||||||
|
|
||||||
|
$ coder organization -O <organization_name> roles create --stidin < role.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### -y, --yes
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|------|-------------------|
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
|
||||||
|
Bypass prompts.
|
||||||
|
|
||||||
|
### --dry-run
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|------|-------------------|
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
|
||||||
|
Does all the work, but does not submit the final updated role.
|
||||||
|
|
||||||
|
### --stdin
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|------|-------------------|
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
|
||||||
|
Reads stdin for the json role definition to upload.
|
@ -1,12 +1,12 @@
|
|||||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||||
# organizations roles edit
|
# organizations roles update
|
||||||
|
|
||||||
Edit an organization custom role
|
Update an organization custom role
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```console
|
```console
|
||||||
coder organizations roles edit [flags] <role_name>
|
coder organizations roles update [flags] <role_name>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
@ -14,7 +14,7 @@ coder organizations roles edit [flags] <role_name>
|
|||||||
```console
|
```console
|
||||||
- Run with an input.json file:
|
- Run with an input.json file:
|
||||||
|
|
||||||
$ coder roles edit --stdin < role.json
|
$ coder roles update --stdin < role.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
@ -5,10 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/cli/clitest"
|
"github.com/coder/coder/v2/cli/clitest"
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
@ -17,7 +20,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEditOrganizationRoles(t *testing.T) {
|
func TestCreateOrganizationRoles(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Unit test uses --stdin and json as the role input. The interactive cli would
|
// Unit test uses --stdin and json as the role input. The interactive cli would
|
||||||
@ -34,7 +37,7 @@ func TestEditOrganizationRoles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
inv, root := clitest.New(t, "organization", "roles", "edit", "--stdin")
|
inv, root := clitest.New(t, "organization", "roles", "create", "--stdin")
|
||||||
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
||||||
"name": "new-role",
|
"name": "new-role",
|
||||||
"organization_id": "%s",
|
"organization_id": "%s",
|
||||||
@ -72,7 +75,7 @@ func TestEditOrganizationRoles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
inv, root := clitest.New(t, "organization", "roles", "edit", "--stdin")
|
inv, root := clitest.New(t, "organization", "roles", "create", "--stdin")
|
||||||
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
||||||
"name": "new-role",
|
"name": "new-role",
|
||||||
"organization_id": "%s",
|
"organization_id": "%s",
|
||||||
@ -185,3 +188,104 @@ func TestShowOrganizations(t *testing.T) {
|
|||||||
pty.ExpectMatch(orgs["bar"].ID.String())
|
pty.ExpectMatch(orgs["bar"].ID.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateOrganizationRoles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("JSON", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureCustomRoles: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleOwner())
|
||||||
|
|
||||||
|
// Create a role in the DB with no permissions
|
||||||
|
const expectedRole = "test-role"
|
||||||
|
dbgen.CustomRole(t, db, database.CustomRole{
|
||||||
|
Name: expectedRole,
|
||||||
|
DisplayName: "Expected",
|
||||||
|
SitePermissions: nil,
|
||||||
|
OrgPermissions: nil,
|
||||||
|
UserPermissions: nil,
|
||||||
|
OrganizationID: uuid.NullUUID{
|
||||||
|
UUID: owner.OrganizationID,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the new role via JSON
|
||||||
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
inv, root := clitest.New(t, "organization", "roles", "update", "--stdin")
|
||||||
|
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
||||||
|
"name": "test-role",
|
||||||
|
"organization_id": "%s",
|
||||||
|
"display_name": "",
|
||||||
|
"site_permissions": [],
|
||||||
|
"organization_permissions": [
|
||||||
|
{
|
||||||
|
"resource_type": "workspace",
|
||||||
|
"action": "read"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"user_permissions": [],
|
||||||
|
"assignable": false,
|
||||||
|
"built_in": false
|
||||||
|
}`, owner.OrganizationID.String()))
|
||||||
|
|
||||||
|
//nolint:gocritic // only owners can edit roles
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
inv.Stdout = buf
|
||||||
|
err := inv.WithContext(ctx).Run()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, buf.String(), "test-role")
|
||||||
|
require.Contains(t, buf.String(), "1 permissions")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidRole", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ownerClient, _, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureCustomRoles: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleOwner())
|
||||||
|
|
||||||
|
// Update the new role via JSON
|
||||||
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
inv, root := clitest.New(t, "organization", "roles", "update", "--stdin")
|
||||||
|
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
||||||
|
"name": "test-role",
|
||||||
|
"organization_id": "%s",
|
||||||
|
"display_name": "",
|
||||||
|
"site_permissions": [],
|
||||||
|
"organization_permissions": [
|
||||||
|
{
|
||||||
|
"resource_type": "workspace",
|
||||||
|
"action": "read"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"user_permissions": [],
|
||||||
|
"assignable": false,
|
||||||
|
"built_in": false
|
||||||
|
}`, owner.OrganizationID.String()))
|
||||||
|
|
||||||
|
//nolint:gocritic // only owners can edit roles
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
inv.Stdout = buf
|
||||||
|
err := inv.WithContext(ctx).Run()
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorContains(t, err, "The role test-role does not exist.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user