mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
chore: add edit organization role to cli (#13365)
Editing custom org roles from hidden org cli command.
This commit is contained in:
@@ -43,7 +43,10 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
return "", err
|
||||
}
|
||||
|
||||
values, err := MultiSelect(inv, options)
|
||||
values, err := MultiSelect(inv, MultiSelectOptions{
|
||||
Options: options,
|
||||
Defaults: options,
|
||||
})
|
||||
if err == nil {
|
||||
v, err := json.Marshal(&values)
|
||||
if err != nil {
|
||||
|
||||
+13
-4
@@ -56,6 +56,7 @@ type SelectOptions struct {
|
||||
Options []string
|
||||
// Default will be highlighted first if it's a valid option.
|
||||
Default string
|
||||
Message string
|
||||
Size int
|
||||
HideSearch bool
|
||||
}
|
||||
@@ -122,6 +123,7 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
|
||||
Options: opts.Options,
|
||||
Default: defaultOption,
|
||||
PageSize: opts.Size,
|
||||
Message: opts.Message,
|
||||
}, &value, survey.WithIcons(func(is *survey.IconSet) {
|
||||
is.Help.Text = "Type to search"
|
||||
if opts.HideSearch {
|
||||
@@ -138,15 +140,22 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
|
||||
return value, err
|
||||
}
|
||||
|
||||
func MultiSelect(inv *serpent.Invocation, items []string) ([]string, error) {
|
||||
type MultiSelectOptions struct {
|
||||
Message string
|
||||
Options []string
|
||||
Defaults []string
|
||||
}
|
||||
|
||||
func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) {
|
||||
// Similar hack is applied to Select()
|
||||
if flag.Lookup("test.v") != nil {
|
||||
return items, nil
|
||||
return opts.Defaults, nil
|
||||
}
|
||||
|
||||
prompt := &survey.MultiSelect{
|
||||
Options: items,
|
||||
Default: items,
|
||||
Options: opts.Options,
|
||||
Default: opts.Defaults,
|
||||
Message: opts.Message,
|
||||
}
|
||||
|
||||
var values []string
|
||||
|
||||
@@ -107,7 +107,10 @@ func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
|
||||
var values []string
|
||||
cmd := &serpent.Command{
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
selectedItems, err := cliui.MultiSelect(inv, items)
|
||||
selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
|
||||
Options: items,
|
||||
Defaults: items,
|
||||
})
|
||||
if err == nil {
|
||||
values = selectedItems
|
||||
}
|
||||
|
||||
+300
-17
@@ -1,13 +1,17 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
@@ -23,6 +27,7 @@ func (r *RootCmd) organizationRoles() *serpent.Command {
|
||||
Hidden: true,
|
||||
Children: []*serpent.Command{
|
||||
r.showOrganizationRoles(),
|
||||
r.editOrganizationRole(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
@@ -31,25 +36,19 @@ func (r *RootCmd) organizationRoles() *serpent.Command {
|
||||
func (r *RootCmd) showOrganizationRoles() *serpent.Command {
|
||||
formatter := cliui.NewOutputFormatter(
|
||||
cliui.ChangeFormatterData(
|
||||
cliui.TableFormat([]assignableRolesTableRow{}, []string{"name", "display_name", "built_in", "site_permissions", "org_permissions", "user_permissions"}),
|
||||
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "org_permissions", "user_permissions"}),
|
||||
func(data any) (any, error) {
|
||||
input, ok := data.([]codersdk.AssignableRoles)
|
||||
inputs, ok := data.([]codersdk.AssignableRoles)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("expected []codersdk.AssignableRoles got %T", data)
|
||||
}
|
||||
rows := make([]assignableRolesTableRow, 0, len(input))
|
||||
for _, role := range input {
|
||||
rows = append(rows, assignableRolesTableRow{
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: fmt.Sprintf("%d permissions", len(role.SitePermissions)),
|
||||
OrganizationPermissions: fmt.Sprintf("%d organizations", len(role.OrganizationPermissions)),
|
||||
UserPermissions: fmt.Sprintf("%d permissions", len(role.UserPermissions)),
|
||||
Assignable: role.Assignable,
|
||||
BuiltIn: role.BuiltIn,
|
||||
})
|
||||
|
||||
tableRows := make([]roleTableRow, 0)
|
||||
for _, input := range inputs {
|
||||
tableRows = append(tableRows, roleToTableView(input.Role))
|
||||
}
|
||||
return rows, nil
|
||||
|
||||
return tableRows, nil
|
||||
},
|
||||
),
|
||||
cliui.JSONFormat(),
|
||||
@@ -101,13 +100,297 @@ func (r *RootCmd) showOrganizationRoles() *serpent.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
type assignableRolesTableRow struct {
|
||||
func (r *RootCmd) editOrganizationRole() *serpent.Command {
|
||||
formatter := cliui.NewOutputFormatter(
|
||||
cliui.ChangeFormatterData(
|
||||
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "org_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: "edit <role_name>",
|
||||
Short: "Edit an organization custom role",
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Description: "Run with an input.json file",
|
||||
Command: "coder roles edit --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 := CurrentOrganization(r, inv, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var customRole codersdk.Role
|
||||
if jsonInput {
|
||||
// JSON Upload mode
|
||||
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("the input appears to be an array, only 1 role can be sent at a time")
|
||||
}
|
||||
return xerrors.Errorf("json input does not appear to be a valid role")
|
||||
}
|
||||
} else {
|
||||
if len(inv.Args) == 0 {
|
||||
return xerrors.Errorf("missing role name argument, usage: \"coder organizations roles edit <role_name>\"")
|
||||
}
|
||||
|
||||
interactiveRole, err := interactiveOrgRoleEdit(inv, org.ID, client)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("editing role: %w", err)
|
||||
}
|
||||
|
||||
customRole = *interactiveRole
|
||||
|
||||
preview := fmt.Sprintf("permissions: %d site, %d org, %d user",
|
||||
len(customRole.SitePermissions), len(customRole.OrganizationPermissions), len(customRole.UserPermissions))
|
||||
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Are you sure you wish to update the role? " + preview,
|
||||
Default: "yes",
|
||||
IsConfirm: true,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("abort: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var updated codersdk.Role
|
||||
if dryRun {
|
||||
// Do not actually post
|
||||
updated = customRole
|
||||
} else {
|
||||
updated, err = client.PatchOrganizationRole(ctx, org.ID, customRole)
|
||||
if err != nil {
|
||||
return fmt.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
|
||||
},
|
||||
}
|
||||
|
||||
formatter.AttachOptions(&cmd.Options)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func interactiveOrgRoleEdit(inv *serpent.Invocation, orgID uuid.UUID, client *codersdk.Client) (*codersdk.Role, error) {
|
||||
ctx := inv.Context()
|
||||
roles, err := client.ListOrganizationRoles(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, 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, xerrors.Errorf("abort: %w", err)
|
||||
}
|
||||
|
||||
originalRole.Role = codersdk.Role{
|
||||
Name: inv.Args[0],
|
||||
OrganizationID: orgID.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Some checks since interactive mode is limited in what it currently sees
|
||||
if len(originalRole.SitePermissions) > 0 {
|
||||
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains site wide permissions")
|
||||
}
|
||||
|
||||
if len(originalRole.UserPermissions) > 0 {
|
||||
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains user permissions")
|
||||
}
|
||||
|
||||
role := &originalRole.Role
|
||||
allowedResources := []codersdk.RBACResource{
|
||||
codersdk.ResourceTemplate,
|
||||
codersdk.ResourceWorkspace,
|
||||
codersdk.ResourceUser,
|
||||
codersdk.ResourceGroup,
|
||||
}
|
||||
|
||||
const done = "Finish and submit changes"
|
||||
const abort = "Cancel changes"
|
||||
|
||||
// Now starts the role editing "game".
|
||||
customRoleLoop:
|
||||
for {
|
||||
selected, err := cliui.Select(inv, cliui.SelectOptions{
|
||||
Message: "Select which resources to edit permissions",
|
||||
Options: append(permissionPreviews(role, allowedResources), done, abort),
|
||||
})
|
||||
if err != nil {
|
||||
return role, xerrors.Errorf("selecting resource: %w", err)
|
||||
}
|
||||
switch selected {
|
||||
case done:
|
||||
break customRoleLoop
|
||||
case abort:
|
||||
return role, xerrors.Errorf("edit role %q aborted", role.Name)
|
||||
default:
|
||||
strs := strings.Split(selected, "::")
|
||||
resource := strings.TrimSpace(strs[0])
|
||||
|
||||
actions, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
|
||||
Message: fmt.Sprintf("Select actions to allow across the whole deployment for resources=%q", resource),
|
||||
Options: slice.ToStrings(codersdk.RBACResourceActions[codersdk.RBACResource(resource)]),
|
||||
Defaults: defaultActions(role, resource),
|
||||
})
|
||||
if err != nil {
|
||||
return role, xerrors.Errorf("selecting actions for resource %q: %w", resource, err)
|
||||
}
|
||||
applyOrgResourceActions(role, resource, actions)
|
||||
// back to resources!
|
||||
}
|
||||
}
|
||||
// This println is required because the prompt ends us on the same line as some text.
|
||||
_, _ = fmt.Println()
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func applyOrgResourceActions(role *codersdk.Role, resource string, actions []string) {
|
||||
if role.OrganizationPermissions == nil {
|
||||
role.OrganizationPermissions = make([]codersdk.Permission, 0)
|
||||
}
|
||||
|
||||
// Construct new site perms with only new perms for the resource
|
||||
keep := make([]codersdk.Permission, 0)
|
||||
for _, perm := range role.OrganizationPermissions {
|
||||
perm := perm
|
||||
if string(perm.ResourceType) != resource {
|
||||
keep = append(keep, perm)
|
||||
}
|
||||
}
|
||||
|
||||
// Add new perms
|
||||
for _, action := range actions {
|
||||
keep = append(keep, codersdk.Permission{
|
||||
Negate: false,
|
||||
ResourceType: codersdk.RBACResource(resource),
|
||||
Action: codersdk.RBACAction(action),
|
||||
})
|
||||
}
|
||||
|
||||
role.OrganizationPermissions = keep
|
||||
}
|
||||
|
||||
func defaultActions(role *codersdk.Role, resource string) []string {
|
||||
if role.OrganizationPermissions == nil {
|
||||
role.OrganizationPermissions = []codersdk.Permission{}
|
||||
}
|
||||
|
||||
defaults := make([]string, 0)
|
||||
for _, perm := range role.OrganizationPermissions {
|
||||
if string(perm.ResourceType) == resource {
|
||||
defaults = append(defaults, string(perm.Action))
|
||||
}
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
func permissionPreviews(role *codersdk.Role, resources []codersdk.RBACResource) []string {
|
||||
previews := make([]string, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
previews = append(previews, permissionPreview(role, resource))
|
||||
}
|
||||
return previews
|
||||
}
|
||||
|
||||
func permissionPreview(role *codersdk.Role, resource codersdk.RBACResource) string {
|
||||
if role.OrganizationPermissions == nil {
|
||||
role.OrganizationPermissions = []codersdk.Permission{}
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, perm := range role.OrganizationPermissions {
|
||||
if perm.ResourceType == resource {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s :: %d permissions", resource, count)
|
||||
}
|
||||
|
||||
func roleToTableView(role codersdk.Role) roleTableRow {
|
||||
return roleTableRow{
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
OrganizationID: role.OrganizationID,
|
||||
SitePermissions: fmt.Sprintf("%d permissions", len(role.SitePermissions)),
|
||||
OrganizationPermissions: fmt.Sprintf("%d permissions", len(role.OrganizationPermissions)),
|
||||
UserPermissions: fmt.Sprintf("%d permissions", len(role.UserPermissions)),
|
||||
}
|
||||
}
|
||||
|
||||
type roleTableRow struct {
|
||||
Name string `table:"name,default_sort"`
|
||||
DisplayName string `table:"display_name"`
|
||||
OrganizationID string `table:"organization_id"`
|
||||
SitePermissions string ` table:"site_permissions"`
|
||||
// map[<org_id>] -> Permissions
|
||||
OrganizationPermissions string `table:"org_permissions"`
|
||||
UserPermissions string `table:"user_permissions"`
|
||||
Assignable bool `table:"assignable"`
|
||||
BuiltIn bool `table:"built_in"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,15 @@ import (
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// ToStrings works for any type where the base type is a string.
|
||||
func ToStrings[T ~string](a []T) []string {
|
||||
tmp := make([]string, 0, len(a))
|
||||
for _, v := range a {
|
||||
tmp = append(tmp, string(v))
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
|
||||
// Omit creates a new slice with the arguments omitted from the list.
|
||||
func Omit[T comparable](a []T, omits ...T) []T {
|
||||
tmp := make([]T, 0, len(a))
|
||||
|
||||
@@ -48,3 +48,33 @@ const (
|
||||
ActionWorkspaceStart RBACAction = "start"
|
||||
ActionWorkspaceStop RBACAction = "stop"
|
||||
)
|
||||
|
||||
// RBACResourceActions is the mapping of resources to which actions are valid for
|
||||
// said resource type.
|
||||
var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
ResourceWildcard: []RBACAction{},
|
||||
ResourceApiKey: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceAssignOrgRole: []RBACAction{ActionAssign, ActionDelete, ActionRead},
|
||||
ResourceAssignRole: []RBACAction{ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
||||
ResourceAuditLog: []RBACAction{ActionCreate, ActionRead},
|
||||
ResourceDebugInfo: []RBACAction{ActionRead},
|
||||
ResourceDeploymentConfig: []RBACAction{ActionRead, ActionUpdate},
|
||||
ResourceDeploymentStats: []RBACAction{ActionRead},
|
||||
ResourceFile: []RBACAction{ActionCreate, ActionRead},
|
||||
ResourceGroup: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceLicense: []RBACAction{ActionCreate, ActionDelete, ActionRead},
|
||||
ResourceOauth2App: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceOauth2AppCodeToken: []RBACAction{ActionCreate, ActionDelete, ActionRead},
|
||||
ResourceOauth2AppSecret: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceOrganization: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceOrganizationMember: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceProvisionerDaemon: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceReplicas: []RBACAction{ActionRead},
|
||||
ResourceSystem: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceTailnetCoordinator: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceTemplate: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionViewInsights},
|
||||
ResourceUser: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
|
||||
ResourceWorkspace: []RBACAction{ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
||||
ResourceWorkspaceDormant: []RBACAction{ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
||||
ResourceWorkspaceProxy: []RBACAction{ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestEditOrganizationRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Unit test uses --stdin and json as the role input. The interactive cli would
|
||||
// be hard to drive from a unit test.
|
||||
t.Run("JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
client, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureCustomRoles: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
inv, root := clitest.New(t, "organization", "roles", "edit", "--stdin")
|
||||
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
||||
"name": "new-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(), "new-role")
|
||||
})
|
||||
|
||||
t.Run("InvalidRole", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
client, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureCustomRoles: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
inv, root := clitest.New(t, "organization", "roles", "edit", "--stdin")
|
||||
inv.Stdin = bytes.NewBufferString(fmt.Sprintf(`{
|
||||
"name": "new-role",
|
||||
"organization_id": "%s",
|
||||
"display_name": "",
|
||||
"site_permissions": [
|
||||
{
|
||||
"resource_type": "workspace",
|
||||
"action": "read"
|
||||
}
|
||||
],
|
||||
"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.ErrorContains(t, err, "not allowed to assign site wide permissions for an organization role")
|
||||
})
|
||||
}
|
||||
@@ -16,3 +16,15 @@ const (
|
||||
{{ $element.Enum }} RBACAction = "{{ $element.Value }}"
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
// RBACResourceActions is the mapping of resources to which actions are valid for
|
||||
// said resource type.
|
||||
var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
{{- range $element := . }}
|
||||
Resource{{ pascalCaseName $element.FunctionName }}: []RBACAction{
|
||||
{{- range $actionValue, $_ := $element.Actions }}
|
||||
{{- actionEnum $actionValue -}},
|
||||
{{- end -}}
|
||||
},
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user