mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: include custom roles in list org roles (#13336)
* chore: include custom roles in list org roles * move cli show roles to org scope
This commit is contained in:
@@ -30,6 +30,7 @@ func (r *RootCmd) organizations() *serpent.Command {
|
|||||||
r.currentOrganization(),
|
r.currentOrganization(),
|
||||||
r.switchOrganization(),
|
r.switchOrganization(),
|
||||||
r.createOrganization(),
|
r.createOrganization(),
|
||||||
|
r.organizationRoles(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,26 +12,23 @@ import (
|
|||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
)
|
)
|
||||||
|
|
||||||
// **NOTE** Only covers site wide roles at present. Org scoped roles maybe
|
func (r *RootCmd) organizationRoles() *serpent.Command {
|
||||||
// should be nested under some command that scopes to an org??
|
|
||||||
|
|
||||||
func (r *RootCmd) roles() *serpent.Command {
|
|
||||||
cmd := &serpent.Command{
|
cmd := &serpent.Command{
|
||||||
Use: "roles",
|
Use: "roles",
|
||||||
Short: "Manage site-wide roles.",
|
Short: "Manage organization roles.",
|
||||||
Aliases: []string{"role"},
|
Aliases: []string{"role"},
|
||||||
Handler: func(inv *serpent.Invocation) error {
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
return inv.Command.HelpHandler(inv)
|
return inv.Command.HelpHandler(inv)
|
||||||
},
|
},
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Children: []*serpent.Command{
|
Children: []*serpent.Command{
|
||||||
r.showRole(),
|
r.showOrganizationRoles(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootCmd) showRole() *serpent.Command {
|
func (r *RootCmd) showOrganizationRoles() *serpent.Command {
|
||||||
formatter := cliui.NewOutputFormatter(
|
formatter := cliui.NewOutputFormatter(
|
||||||
cliui.ChangeFormatterData(
|
cliui.ChangeFormatterData(
|
||||||
cliui.TableFormat([]assignableRolesTableRow{}, []string{"name", "display_name", "built_in", "site_permissions", "org_permissions", "user_permissions"}),
|
cliui.TableFormat([]assignableRolesTableRow{}, []string{"name", "display_name", "built_in", "site_permissions", "org_permissions", "user_permissions"}),
|
||||||
@@ -67,7 +64,12 @@ func (r *RootCmd) showRole() *serpent.Command {
|
|||||||
),
|
),
|
||||||
Handler: func(inv *serpent.Invocation) error {
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
roles, err := client.ListSiteRoles(ctx)
|
org, err := CurrentOrganization(r, inv, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := client.ListOrganizationRoles(ctx, org.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("listing roles: %w", err)
|
return xerrors.Errorf("listing roles: %w", err)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/cli/clitest"
|
||||||
|
"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/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShowOrganizationRoles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{})
|
||||||
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
inv, root := clitest.New(t, "organization", "roles", "show")
|
||||||
|
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(), expectedRole)
|
||||||
|
})
|
||||||
|
}
|
||||||
Generated
+8
@@ -8421,6 +8421,10 @@ const docTemplate = `{
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"organization_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -11241,6 +11245,10 @@ const docTemplate = `{
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"organization_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
Generated
+8
@@ -7476,6 +7476,10 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"organization_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -10133,6 +10137,10 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"organization_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
"description": "map[\u003corg_id\u003e] -\u003e Permissions",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -527,12 +527,17 @@ func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.Provisioner
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Role(role rbac.Role) codersdk.Role {
|
func Role(role rbac.Role) codersdk.Role {
|
||||||
|
roleName, orgIDStr, err := rbac.RoleSplit(role.Name)
|
||||||
|
if err != nil {
|
||||||
|
roleName = role.Name
|
||||||
|
}
|
||||||
return codersdk.Role{
|
return codersdk.Role{
|
||||||
Name: role.Name,
|
Name: roleName,
|
||||||
|
OrganizationID: orgIDStr,
|
||||||
DisplayName: role.DisplayName,
|
DisplayName: role.DisplayName,
|
||||||
SitePermissions: List(role.Site, Permission),
|
SitePermissions: List(role.Site, Permission),
|
||||||
OrganizationPermissions: Map(role.Org, ListLazy(Permission)),
|
OrganizationPermissions: Map(role.Org, ListLazy(Permission)),
|
||||||
UserPermissions: List(role.Site, Permission),
|
UserPermissions: List(role.User, Permission),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +551,7 @@ func Permission(permission rbac.Permission) codersdk.Permission {
|
|||||||
|
|
||||||
func RoleToRBAC(role codersdk.Role) rbac.Role {
|
func RoleToRBAC(role codersdk.Role) rbac.Role {
|
||||||
return rbac.Role{
|
return rbac.Role{
|
||||||
Name: role.Name,
|
Name: rbac.RoleName(role.Name, role.OrganizationID),
|
||||||
DisplayName: role.DisplayName,
|
DisplayName: role.DisplayName,
|
||||||
Site: List(role.SitePermissions, PermissionToRBAC),
|
Site: List(role.SitePermissions, PermissionToRBAC),
|
||||||
Org: Map(role.OrganizationPermissions, ListLazy(PermissionToRBAC)),
|
Org: Map(role.OrganizationPermissions, ListLazy(PermissionToRBAC)),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -817,6 +818,19 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CustomRole(t testing.TB, db database.Store, seed database.CustomRole) database.CustomRole {
|
||||||
|
role, err := db.UpsertCustomRole(genCtx, database.UpsertCustomRoleParams{
|
||||||
|
Name: takeFirst(seed.Name, strings.ToLower(namesgenerator.GetRandomName(1))),
|
||||||
|
DisplayName: namesgenerator.GetRandomName(1),
|
||||||
|
OrganizationID: seed.OrganizationID,
|
||||||
|
SitePermissions: takeFirstSlice(seed.SitePermissions, []byte("[]")),
|
||||||
|
OrgPermissions: takeFirstSlice(seed.SitePermissions, []byte("{}")),
|
||||||
|
UserPermissions: takeFirstSlice(seed.SitePermissions, []byte("[]")),
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "insert custom role")
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
|
||||||
func must[V any](v V, err error) V {
|
func must[V any](v V, err error) V {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -1187,7 +1187,11 @@ func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesPar
|
|||||||
role := role
|
role := role
|
||||||
if len(arg.LookupRoles) > 0 {
|
if len(arg.LookupRoles) > 0 {
|
||||||
if !slices.ContainsFunc(arg.LookupRoles, func(s string) bool {
|
if !slices.ContainsFunc(arg.LookupRoles, func(s string) bool {
|
||||||
return strings.EqualFold(s, role.Name)
|
roleName := rbac.RoleName(role.Name, "")
|
||||||
|
if role.OrganizationID.UUID != uuid.Nil {
|
||||||
|
roleName = rbac.RoleName(role.Name, role.OrganizationID.UUID.String())
|
||||||
|
}
|
||||||
|
return strings.EqualFold(s, roleName)
|
||||||
}) {
|
}) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1197,6 +1201,10 @@ func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesPar
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if arg.OrganizationID != uuid.Nil && role.OrganizationID.UUID != arg.OrganizationID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
found = append(found, role)
|
found = append(found, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8377,6 +8385,7 @@ func (q *FakeQuerier) UpsertCustomRole(_ context.Context, arg database.UpsertCus
|
|||||||
for i := range q.customRoles {
|
for i := range q.customRoles {
|
||||||
if strings.EqualFold(q.customRoles[i].Name, arg.Name) {
|
if strings.EqualFold(q.customRoles[i].Name, arg.Name) {
|
||||||
q.customRoles[i].DisplayName = arg.DisplayName
|
q.customRoles[i].DisplayName = arg.DisplayName
|
||||||
|
q.customRoles[i].OrganizationID = arg.OrganizationID
|
||||||
q.customRoles[i].SitePermissions = arg.SitePermissions
|
q.customRoles[i].SitePermissions = arg.SitePermissions
|
||||||
q.customRoles[i].OrgPermissions = arg.OrgPermissions
|
q.customRoles[i].OrgPermissions = arg.OrgPermissions
|
||||||
q.customRoles[i].UserPermissions = arg.UserPermissions
|
q.customRoles[i].UserPermissions = arg.UserPermissions
|
||||||
@@ -8388,6 +8397,7 @@ func (q *FakeQuerier) UpsertCustomRole(_ context.Context, arg database.UpsertCus
|
|||||||
role := database.CustomRole{
|
role := database.CustomRole{
|
||||||
Name: arg.Name,
|
Name: arg.Name,
|
||||||
DisplayName: arg.DisplayName,
|
DisplayName: arg.DisplayName,
|
||||||
|
OrganizationID: arg.OrganizationID,
|
||||||
SitePermissions: arg.SitePermissions,
|
SitePermissions: arg.SitePermissions,
|
||||||
OrgPermissions: arg.OrgPermissions,
|
OrgPermissions: arg.OrgPermissions,
|
||||||
UserPermissions: arg.UserPermissions,
|
UserPermissions: arg.UserPermissions,
|
||||||
|
|||||||
@@ -5604,10 +5604,13 @@ FROM
|
|||||||
custom_roles
|
custom_roles
|
||||||
WHERE
|
WHERE
|
||||||
true
|
true
|
||||||
-- Lookup roles filter
|
-- Lookup roles filter expects the role names to be in the rbac package
|
||||||
|
-- format. Eg: name[:<organization_id>]
|
||||||
AND CASE WHEN array_length($1 :: text[], 1) > 0 THEN
|
AND CASE WHEN array_length($1 :: text[], 1) > 0 THEN
|
||||||
-- Case insensitive
|
-- Case insensitive lookup with org_id appended (if non-null).
|
||||||
name ILIKE ANY($1 :: text [])
|
-- This will return just the name if org_id is null. It'll append
|
||||||
|
-- the org_id if not null
|
||||||
|
concat(name, NULLIF(concat(':', organization_id), ':')) ILIKE ANY($1 :: text [])
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Org scoping filter, to only fetch site wide roles
|
-- Org scoping filter, to only fetch site wide roles
|
||||||
@@ -5615,15 +5618,20 @@ WHERE
|
|||||||
organization_id IS null
|
organization_id IS null
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
|
AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||||
|
organization_id = $3
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
`
|
`
|
||||||
|
|
||||||
type CustomRolesParams struct {
|
type CustomRolesParams struct {
|
||||||
LookupRoles []string `db:"lookup_roles" json:"lookup_roles"`
|
LookupRoles []string `db:"lookup_roles" json:"lookup_roles"`
|
||||||
ExcludeOrgRoles bool `db:"exclude_org_roles" json:"exclude_org_roles"`
|
ExcludeOrgRoles bool `db:"exclude_org_roles" json:"exclude_org_roles"`
|
||||||
|
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) {
|
func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, customRoles, pq.Array(arg.LookupRoles), arg.ExcludeOrgRoles)
|
rows, err := q.db.QueryContext(ctx, customRoles, pq.Array(arg.LookupRoles), arg.ExcludeOrgRoles, arg.OrganizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -5659,6 +5667,7 @@ INSERT INTO
|
|||||||
custom_roles (
|
custom_roles (
|
||||||
name,
|
name,
|
||||||
display_name,
|
display_name,
|
||||||
|
organization_id,
|
||||||
site_permissions,
|
site_permissions,
|
||||||
org_permissions,
|
org_permissions,
|
||||||
user_permissions,
|
user_permissions,
|
||||||
@@ -5672,15 +5681,16 @@ VALUES (
|
|||||||
$3,
|
$3,
|
||||||
$4,
|
$4,
|
||||||
$5,
|
$5,
|
||||||
|
$6,
|
||||||
now(),
|
now(),
|
||||||
now()
|
now()
|
||||||
)
|
)
|
||||||
ON CONFLICT (name)
|
ON CONFLICT (name)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
display_name = $2,
|
display_name = $2,
|
||||||
site_permissions = $3,
|
site_permissions = $4,
|
||||||
org_permissions = $4,
|
org_permissions = $5,
|
||||||
user_permissions = $5,
|
user_permissions = $6,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id
|
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id
|
||||||
`
|
`
|
||||||
@@ -5688,6 +5698,7 @@ RETURNING name, display_name, site_permissions, org_permissions, user_permission
|
|||||||
type UpsertCustomRoleParams struct {
|
type UpsertCustomRoleParams struct {
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
DisplayName string `db:"display_name" json:"display_name"`
|
DisplayName string `db:"display_name" json:"display_name"`
|
||||||
|
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
|
||||||
SitePermissions json.RawMessage `db:"site_permissions" json:"site_permissions"`
|
SitePermissions json.RawMessage `db:"site_permissions" json:"site_permissions"`
|
||||||
OrgPermissions json.RawMessage `db:"org_permissions" json:"org_permissions"`
|
OrgPermissions json.RawMessage `db:"org_permissions" json:"org_permissions"`
|
||||||
UserPermissions json.RawMessage `db:"user_permissions" json:"user_permissions"`
|
UserPermissions json.RawMessage `db:"user_permissions" json:"user_permissions"`
|
||||||
@@ -5697,6 +5708,7 @@ func (q *sqlQuerier) UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleP
|
|||||||
row := q.db.QueryRowContext(ctx, upsertCustomRole,
|
row := q.db.QueryRowContext(ctx, upsertCustomRole,
|
||||||
arg.Name,
|
arg.Name,
|
||||||
arg.DisplayName,
|
arg.DisplayName,
|
||||||
|
arg.OrganizationID,
|
||||||
arg.SitePermissions,
|
arg.SitePermissions,
|
||||||
arg.OrgPermissions,
|
arg.OrgPermissions,
|
||||||
arg.UserPermissions,
|
arg.UserPermissions,
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ FROM
|
|||||||
custom_roles
|
custom_roles
|
||||||
WHERE
|
WHERE
|
||||||
true
|
true
|
||||||
-- Lookup roles filter
|
-- Lookup roles filter expects the role names to be in the rbac package
|
||||||
|
-- format. Eg: name[:<organization_id>]
|
||||||
AND CASE WHEN array_length(@lookup_roles :: text[], 1) > 0 THEN
|
AND CASE WHEN array_length(@lookup_roles :: text[], 1) > 0 THEN
|
||||||
-- Case insensitive
|
-- Case insensitive lookup with org_id appended (if non-null).
|
||||||
name ILIKE ANY(@lookup_roles :: text [])
|
-- This will return just the name if org_id is null. It'll append
|
||||||
|
-- the org_id if not null
|
||||||
|
concat(name, NULLIF(concat(':', organization_id), ':')) ILIKE ANY(@lookup_roles :: text [])
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Org scoping filter, to only fetch site wide roles
|
-- Org scoping filter, to only fetch site wide roles
|
||||||
@@ -16,6 +19,10 @@ WHERE
|
|||||||
organization_id IS null
|
organization_id IS null
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
|
AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
|
||||||
|
organization_id = @organization_id
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
;
|
;
|
||||||
|
|
||||||
-- name: UpsertCustomRole :one
|
-- name: UpsertCustomRole :one
|
||||||
@@ -23,6 +30,7 @@ INSERT INTO
|
|||||||
custom_roles (
|
custom_roles (
|
||||||
name,
|
name,
|
||||||
display_name,
|
display_name,
|
||||||
|
organization_id,
|
||||||
site_permissions,
|
site_permissions,
|
||||||
org_permissions,
|
org_permissions,
|
||||||
user_permissions,
|
user_permissions,
|
||||||
@@ -33,6 +41,7 @@ VALUES (
|
|||||||
-- Always force lowercase names
|
-- Always force lowercase names
|
||||||
lower(@name),
|
lower(@name),
|
||||||
@display_name,
|
@display_name,
|
||||||
|
@organization_id,
|
||||||
@site_permissions,
|
@site_permissions,
|
||||||
@org_permissions,
|
@org_permissions,
|
||||||
@user_permissions,
|
@user_permissions,
|
||||||
|
|||||||
+18
-18
@@ -53,29 +53,29 @@ func (names RoleNames) Names() []string {
|
|||||||
// site and orgs, and these functions can be removed.
|
// site and orgs, and these functions can be removed.
|
||||||
|
|
||||||
func RoleOwner() string {
|
func RoleOwner() string {
|
||||||
return roleName(owner, "")
|
return RoleName(owner, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func CustomSiteRole() string { return roleName(customSiteRole, "") }
|
func CustomSiteRole() string { return RoleName(customSiteRole, "") }
|
||||||
|
|
||||||
func RoleTemplateAdmin() string {
|
func RoleTemplateAdmin() string {
|
||||||
return roleName(templateAdmin, "")
|
return RoleName(templateAdmin, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func RoleUserAdmin() string {
|
func RoleUserAdmin() string {
|
||||||
return roleName(userAdmin, "")
|
return RoleName(userAdmin, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func RoleMember() string {
|
func RoleMember() string {
|
||||||
return roleName(member, "")
|
return RoleName(member, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func RoleOrgAdmin(organizationID uuid.UUID) string {
|
func RoleOrgAdmin(organizationID uuid.UUID) string {
|
||||||
return roleName(orgAdmin, organizationID.String())
|
return RoleName(orgAdmin, organizationID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func RoleOrgMember(organizationID uuid.UUID) string {
|
func RoleOrgMember(organizationID uuid.UUID) string {
|
||||||
return roleName(orgMember, organizationID.String())
|
return RoleName(orgMember, organizationID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func allPermsExcept(excepts ...Objecter) []Permission {
|
func allPermsExcept(excepts ...Objecter) []Permission {
|
||||||
@@ -273,7 +273,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||||||
// organization scope.
|
// organization scope.
|
||||||
orgAdmin: func(organizationID string) Role {
|
orgAdmin: func(organizationID string) Role {
|
||||||
return Role{
|
return Role{
|
||||||
Name: roleName(orgAdmin, organizationID),
|
Name: RoleName(orgAdmin, organizationID),
|
||||||
DisplayName: "Organization Admin",
|
DisplayName: "Organization Admin",
|
||||||
Site: []Permission{},
|
Site: []Permission{},
|
||||||
Org: map[string][]Permission{
|
Org: map[string][]Permission{
|
||||||
@@ -291,7 +291,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||||||
// in an organization.
|
// in an organization.
|
||||||
orgMember: func(organizationID string) Role {
|
orgMember: func(organizationID string) Role {
|
||||||
return Role{
|
return Role{
|
||||||
Name: roleName(orgMember, organizationID),
|
Name: RoleName(orgMember, organizationID),
|
||||||
DisplayName: "",
|
DisplayName: "",
|
||||||
Site: []Permission{},
|
Site: []Permission{},
|
||||||
Org: map[string][]Permission{
|
Org: map[string][]Permission{
|
||||||
@@ -475,13 +475,13 @@ func CanAssignRole(expandable ExpandableRoles, assignedRole string) bool {
|
|||||||
// For CanAssignRole, we only care about the names of the roles.
|
// For CanAssignRole, we only care about the names of the roles.
|
||||||
roles := expandable.Names()
|
roles := expandable.Names()
|
||||||
|
|
||||||
assigned, assignedOrg, err := roleSplit(assignedRole)
|
assigned, assignedOrg, err := RoleSplit(assignedRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, longRole := range roles {
|
for _, longRole := range roles {
|
||||||
role, orgID, err := roleSplit(longRole)
|
role, orgID, err := RoleSplit(longRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,7 @@ func CanAssignRole(expandable ExpandableRoles, assignedRole string) bool {
|
|||||||
// api. We should maybe make an exported function that returns just the
|
// api. We should maybe make an exported function that returns just the
|
||||||
// human-readable content of the Role struct (name + display name).
|
// human-readable content of the Role struct (name + display name).
|
||||||
func RoleByName(name string) (Role, error) {
|
func RoleByName(name string) (Role, error) {
|
||||||
roleName, orgID, err := roleSplit(name)
|
roleName, orgID, err := RoleSplit(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Role{}, xerrors.Errorf("parse role name: %w", err)
|
return Role{}, xerrors.Errorf("parse role name: %w", err)
|
||||||
}
|
}
|
||||||
@@ -544,7 +544,7 @@ func rolesByNames(roleNames []string) ([]Role, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsOrgRole(roleName string) (string, bool) {
|
func IsOrgRole(roleName string) (string, bool) {
|
||||||
_, orgID, err := roleSplit(roleName)
|
_, orgID, err := RoleSplit(roleName)
|
||||||
if err == nil && orgID != "" {
|
if err == nil && orgID != "" {
|
||||||
return orgID, true
|
return orgID, true
|
||||||
}
|
}
|
||||||
@@ -561,7 +561,7 @@ func OrganizationRoles(organizationID uuid.UUID) []Role {
|
|||||||
var roles []Role
|
var roles []Role
|
||||||
for _, roleF := range builtInRoles {
|
for _, roleF := range builtInRoles {
|
||||||
role := roleF(organizationID.String())
|
role := roleF(organizationID.String())
|
||||||
_, scope, err := roleSplit(role.Name)
|
_, scope, err := RoleSplit(role.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This should never happen
|
// This should never happen
|
||||||
continue
|
continue
|
||||||
@@ -582,7 +582,7 @@ func SiteRoles() []Role {
|
|||||||
var roles []Role
|
var roles []Role
|
||||||
for _, roleF := range builtInRoles {
|
for _, roleF := range builtInRoles {
|
||||||
role := roleF("random")
|
role := roleF("random")
|
||||||
_, scope, err := roleSplit(role.Name)
|
_, scope, err := RoleSplit(role.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This should never happen
|
// This should never happen
|
||||||
continue
|
continue
|
||||||
@@ -625,19 +625,19 @@ func ChangeRoleSet(from []string, to []string) (added []string, removed []string
|
|||||||
return added, removed
|
return added, removed
|
||||||
}
|
}
|
||||||
|
|
||||||
// roleName is a quick helper function to return
|
// RoleName is a quick helper function to return
|
||||||
//
|
//
|
||||||
// role_name:scopeID
|
// role_name:scopeID
|
||||||
//
|
//
|
||||||
// If no scopeID is required, only 'role_name' is returned
|
// If no scopeID is required, only 'role_name' is returned
|
||||||
func roleName(name string, orgID string) string {
|
func RoleName(name string, orgID string) string {
|
||||||
if orgID == "" {
|
if orgID == "" {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
return name + ":" + orgID
|
return name + ":" + orgID
|
||||||
}
|
}
|
||||||
|
|
||||||
func roleSplit(role string) (name string, orgID string, err error) {
|
func RoleSplit(role string) (name string, orgID string, err error) {
|
||||||
arr := strings.Split(role, ":")
|
arr := strings.Split(role, ":")
|
||||||
if len(arr) > 2 {
|
if len(arr) > 2 {
|
||||||
return "", "", xerrors.Errorf("too many colons in role name")
|
return "", "", xerrors.Errorf("too many colons in role name")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
@@ -75,6 +76,7 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles,
|
|||||||
dbroles, err := db.CustomRoles(ctx, database.CustomRolesParams{
|
dbroles, err := db.CustomRoles(ctx, database.CustomRolesParams{
|
||||||
LookupRoles: lookup,
|
LookupRoles: lookup,
|
||||||
ExcludeOrgRoles: false,
|
ExcludeOrgRoles: false,
|
||||||
|
OrganizationID: uuid.Nil,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("fetch custom roles: %w", err)
|
return nil, xerrors.Errorf("fetch custom roles: %w", err)
|
||||||
@@ -95,8 +97,12 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
||||||
|
name := dbRole.Name
|
||||||
|
if dbRole.OrganizationID.Valid {
|
||||||
|
name = rbac.RoleName(dbRole.Name, dbRole.OrganizationID.UUID.String())
|
||||||
|
}
|
||||||
role := rbac.Role{
|
role := rbac.Role{
|
||||||
Name: dbRole.Name,
|
Name: name,
|
||||||
DisplayName: dbRole.DisplayName,
|
DisplayName: dbRole.DisplayName,
|
||||||
Site: nil,
|
Site: nil,
|
||||||
Org: nil,
|
Org: nil,
|
||||||
@@ -122,11 +128,27 @@ func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ConvertRoleToDB(role rbac.Role) (database.CustomRole, error) {
|
func ConvertRoleToDB(role rbac.Role) (database.CustomRole, error) {
|
||||||
|
roleName, orgIDStr, err := rbac.RoleSplit(role.Name)
|
||||||
|
if err != nil {
|
||||||
|
return database.CustomRole{}, xerrors.Errorf("split role %q: %w", role.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
dbRole := database.CustomRole{
|
dbRole := database.CustomRole{
|
||||||
Name: role.Name,
|
Name: roleName,
|
||||||
DisplayName: role.DisplayName,
|
DisplayName: role.DisplayName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if orgIDStr != "" {
|
||||||
|
orgID, err := uuid.Parse(orgIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return database.CustomRole{}, xerrors.Errorf("parse org id %q: %w", orgIDStr, err)
|
||||||
|
}
|
||||||
|
dbRole.OrganizationID = uuid.NullUUID{
|
||||||
|
UUID: orgID,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
siteData, err := json.Marshal(role.Site)
|
siteData, err := json.Marshal(role.Site)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dbRole, xerrors.Errorf("marshal site permissions: %w", err)
|
return dbRole, xerrors.Errorf("marshal site permissions: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package rolestore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbmem"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpandCustomRoleRoles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
db := dbmem.New()
|
||||||
|
|
||||||
|
org := dbgen.Organization(t, db, database.Organization{})
|
||||||
|
|
||||||
|
const roleName = "test-role"
|
||||||
|
dbgen.CustomRole(t, db, database.CustomRole{
|
||||||
|
Name: roleName,
|
||||||
|
DisplayName: "",
|
||||||
|
SitePermissions: nil,
|
||||||
|
OrgPermissions: nil,
|
||||||
|
UserPermissions: nil,
|
||||||
|
OrganizationID: uuid.NullUUID{
|
||||||
|
UUID: org.ID,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
roles, err := rolestore.Expand(ctx, db, []string{rbac.RoleName(roleName, org.ID.String())})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, roles, 1, "role found")
|
||||||
|
}
|
||||||
+23
-2
@@ -3,6 +3,8 @@ package coderd
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||||
"github.com/coder/coder/v2/coderd/httpmw"
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
@@ -32,9 +34,10 @@ func (api *API) AssignableSiteRoles(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbCustomRoles, err := api.Database.CustomRoles(ctx, database.CustomRolesParams{
|
dbCustomRoles, err := api.Database.CustomRoles(ctx, database.CustomRolesParams{
|
||||||
|
LookupRoles: nil,
|
||||||
// Only site wide custom roles to be included
|
// Only site wide custom roles to be included
|
||||||
ExcludeOrgRoles: true,
|
ExcludeOrgRoles: true,
|
||||||
LookupRoles: nil,
|
OrganizationID: uuid.Nil,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.InternalServerError(rw, err)
|
httpapi.InternalServerError(rw, err)
|
||||||
@@ -73,7 +76,25 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
roles := rbac.OrganizationRoles(organization.ID)
|
roles := rbac.OrganizationRoles(organization.ID)
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles, []rbac.Role{}))
|
dbCustomRoles, err := api.Database.CustomRoles(ctx, database.CustomRolesParams{
|
||||||
|
LookupRoles: nil,
|
||||||
|
ExcludeOrgRoles: false,
|
||||||
|
OrganizationID: organization.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customRoles := make([]rbac.Role, 0, len(dbCustomRoles))
|
||||||
|
for _, customRole := range dbCustomRoles {
|
||||||
|
rbacRole, err := rolestore.ConvertDBRole(customRole)
|
||||||
|
if err == nil {
|
||||||
|
customRoles = append(customRoles, rbacRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles, customRoles))
|
||||||
}
|
}
|
||||||
|
|
||||||
func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role, customRoles []rbac.Role) []codersdk.AssignableRoles {
|
func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role, customRoles []rbac.Role) []codersdk.AssignableRoles {
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ package coderd_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||||
|
"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/coderd/rbac/policy"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
@@ -156,6 +160,43 @@ func TestListRoles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListCustomRoles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("Organizations", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, db := coderdtest.NewWithDatabase(t, nil)
|
||||||
|
owner := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
|
const roleName = "random_role"
|
||||||
|
dbgen.CustomRole(t, db, must(rolestore.ConvertRoleToDB(rbac.Role{
|
||||||
|
Name: rbac.RoleName(roleName, owner.OrganizationID.String()),
|
||||||
|
DisplayName: "Random Role",
|
||||||
|
Site: nil,
|
||||||
|
Org: map[string][]rbac.Permission{
|
||||||
|
owner.OrganizationID.String(): {
|
||||||
|
{
|
||||||
|
Negate: false,
|
||||||
|
ResourceType: rbac.ResourceWorkspace.Type,
|
||||||
|
Action: policy.ActionRead,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
User: nil,
|
||||||
|
})))
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
roles, err := client.ListOrganizationRoles(ctx, owner.OrganizationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
found := slices.ContainsFunc(roles, func(element codersdk.AssignableRoles) bool {
|
||||||
|
return element.Name == roleName && element.OrganizationID == owner.OrganizationID.String()
|
||||||
|
})
|
||||||
|
require.Truef(t, found, "custom organization role listed")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func convertRole(roleName string) codersdk.Role {
|
func convertRole(roleName string) codersdk.Role {
|
||||||
role, _ := rbac.RoleByName(roleName)
|
role, _ := rbac.RoleByName(roleName)
|
||||||
return db2sdk.Role(role)
|
return db2sdk.Role(role)
|
||||||
|
|||||||
+2
-1
@@ -35,7 +35,8 @@ type Permission struct {
|
|||||||
|
|
||||||
// Role is a longer form of SlimRole used to edit custom roles.
|
// Role is a longer form of SlimRole used to edit custom roles.
|
||||||
type Role struct {
|
type Role struct {
|
||||||
Name string `json:"name" table:"name,default_sort"`
|
Name string `json:"name" table:"name,default_sort" validate:"username"`
|
||||||
|
OrganizationID string `json:"organization_id" table:"organization_id" format:"uuid"`
|
||||||
DisplayName string `json:"display_name" table:"display_name"`
|
DisplayName string `json:"display_name" table:"display_name"`
|
||||||
SitePermissions []Permission `json:"site_permissions" table:"site_permissions"`
|
SitePermissions []Permission `json:"site_permissions" table:"site_permissions"`
|
||||||
// map[<org_id>] -> Permissions
|
// map[<org_id>] -> Permissions
|
||||||
|
|||||||
Generated
+6
@@ -30,6 +30,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
|
|||||||
"built_in": true,
|
"built_in": true,
|
||||||
"display_name": "string",
|
"display_name": "string",
|
||||||
"name": "string",
|
"name": "string",
|
||||||
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"property1": [
|
"property1": [
|
||||||
{
|
{
|
||||||
@@ -81,6 +82,7 @@ Status Code **200**
|
|||||||
| `» built_in` | boolean | false | | Built in roles are immutable |
|
| `» built_in` | boolean | false | | Built in roles are immutable |
|
||||||
| `» display_name` | string | false | | |
|
| `» display_name` | string | false | | |
|
||||||
| `» name` | string | false | | |
|
| `» name` | string | false | | |
|
||||||
|
| `» organization_id` | string(uuid) | false | | |
|
||||||
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||||
| `»» [any property]` | array | false | | |
|
| `»» [any property]` | array | false | | |
|
||||||
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||||
@@ -215,6 +217,7 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \
|
|||||||
"built_in": true,
|
"built_in": true,
|
||||||
"display_name": "string",
|
"display_name": "string",
|
||||||
"name": "string",
|
"name": "string",
|
||||||
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"property1": [
|
"property1": [
|
||||||
{
|
{
|
||||||
@@ -266,6 +269,7 @@ Status Code **200**
|
|||||||
| `» built_in` | boolean | false | | Built in roles are immutable |
|
| `» built_in` | boolean | false | | Built in roles are immutable |
|
||||||
| `» display_name` | string | false | | |
|
| `» display_name` | string | false | | |
|
||||||
| `» name` | string | false | | |
|
| `» name` | string | false | | |
|
||||||
|
| `» organization_id` | string(uuid) | false | | |
|
||||||
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||||
| `»» [any property]` | array | false | | |
|
| `»» [any property]` | array | false | | |
|
||||||
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||||
@@ -341,6 +345,7 @@ curl -X PATCH http://coder-server:8080/api/v2/users/roles \
|
|||||||
{
|
{
|
||||||
"display_name": "string",
|
"display_name": "string",
|
||||||
"name": "string",
|
"name": "string",
|
||||||
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"property1": [
|
"property1": [
|
||||||
{
|
{
|
||||||
@@ -390,6 +395,7 @@ Status Code **200**
|
|||||||
| `[array item]` | array | false | | |
|
| `[array item]` | array | false | | |
|
||||||
| `» display_name` | string | false | | |
|
| `» display_name` | string | false | | |
|
||||||
| `» name` | string | false | | |
|
| `» name` | string | false | | |
|
||||||
|
| `» organization_id` | string(uuid) | false | | |
|
||||||
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
| `» organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||||
| `»» [any property]` | array | false | | |
|
| `»» [any property]` | array | false | | |
|
||||||
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
| `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
|
||||||
|
|||||||
Generated
+4
@@ -805,6 +805,7 @@
|
|||||||
"built_in": true,
|
"built_in": true,
|
||||||
"display_name": "string",
|
"display_name": "string",
|
||||||
"name": "string",
|
"name": "string",
|
||||||
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"property1": [
|
"property1": [
|
||||||
{
|
{
|
||||||
@@ -846,6 +847,7 @@
|
|||||||
| `built_in` | boolean | false | | Built in roles are immutable |
|
| `built_in` | boolean | false | | Built in roles are immutable |
|
||||||
| `display_name` | string | false | | |
|
| `display_name` | string | false | | |
|
||||||
| `name` | string | false | | |
|
| `name` | string | false | | |
|
||||||
|
| `organization_id` | string | false | | |
|
||||||
| `organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
| `organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||||
| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||||
@@ -4327,6 +4329,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
{
|
{
|
||||||
"display_name": "string",
|
"display_name": "string",
|
||||||
"name": "string",
|
"name": "string",
|
||||||
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
"organization_permissions": {
|
"organization_permissions": {
|
||||||
"property1": [
|
"property1": [
|
||||||
{
|
{
|
||||||
@@ -4366,6 +4369,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- |
|
| -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- |
|
||||||
| `display_name` | string | false | | |
|
| `display_name` | string | false | | |
|
||||||
| `name` | string | false | | |
|
| `name` | string | false | | |
|
||||||
|
| `organization_id` | string | false | | |
|
||||||
| `organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
| `organization_permissions` | object | false | | map[<org_id>] -> Permissions |
|
||||||
| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
| » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||||
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package cli_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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/coderd/rbac"
|
|
||||||
"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/pty/ptytest"
|
|
||||||
"github.com/coder/coder/v2/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestShowRoles(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
|
||||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
|
||||||
owner, admin := coderdenttest.New(t, &coderdenttest.Options{
|
|
||||||
Options: &coderdtest.Options{
|
|
||||||
DeploymentValues: dv,
|
|
||||||
},
|
|
||||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
||||||
Features: license.Features{
|
|
||||||
codersdk.FeatureCustomRoles: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Requires an owner
|
|
||||||
client, _ := coderdtest.CreateAnotherUser(t, owner, admin.OrganizationID, rbac.RoleOwner())
|
|
||||||
|
|
||||||
const expectedRole = "test-role"
|
|
||||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
||||||
_, err := client.PatchRole(ctx, codersdk.Role{
|
|
||||||
Name: expectedRole,
|
|
||||||
DisplayName: "Test Role",
|
|
||||||
SitePermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
|
||||||
codersdk.ResourceWorkspace: {codersdk.ActionRead, codersdk.ActionUpdate},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
require.NoError(t, err, "create role")
|
|
||||||
|
|
||||||
inv, conf := newCLI(t, "roles", "show", "test-role")
|
|
||||||
|
|
||||||
pty := ptytest.New(t)
|
|
||||||
inv.Stdout = pty.Output()
|
|
||||||
clitest.SetupConfig(t, client, conf)
|
|
||||||
|
|
||||||
err = inv.Run()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
matches := []string{
|
|
||||||
"test-role", "2 permissions",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, match := range matches {
|
|
||||||
pty.ExpectMatch(match)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ func (r *RootCmd) enterpriseOnly() []*serpent.Command {
|
|||||||
r.licenses(),
|
r.licenses(),
|
||||||
r.groups(),
|
r.groups(),
|
||||||
r.provisionerDaemons(),
|
r.provisionerDaemons(),
|
||||||
r.roles(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package coderd
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||||
"github.com/coder/coder/v2/coderd/httpapi"
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
@@ -59,6 +61,7 @@ func (api *API) patchRole(rw http.ResponseWriter, r *http.Request) {
|
|||||||
inserted, err := api.Database.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
inserted, err := api.Database.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||||
Name: args.Name,
|
Name: args.Name,
|
||||||
DisplayName: args.DisplayName,
|
DisplayName: args.DisplayName,
|
||||||
|
OrganizationID: uuid.NullUUID{},
|
||||||
SitePermissions: args.SitePermissions,
|
SitePermissions: args.SitePermissions,
|
||||||
OrgPermissions: args.OrgPermissions,
|
OrgPermissions: args.OrgPermissions,
|
||||||
UserPermissions: args.UserPermissions,
|
UserPermissions: args.UserPermissions,
|
||||||
|
|||||||
@@ -198,6 +198,6 @@ func TestCustomRole(t *testing.T) {
|
|||||||
OrganizationPermissions: nil,
|
OrganizationPermissions: nil,
|
||||||
UserPermissions: nil,
|
UserPermissions: nil,
|
||||||
})
|
})
|
||||||
require.ErrorContains(t, err, "Invalid role name")
|
require.ErrorContains(t, err, "Validation")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1
@@ -977,6 +977,7 @@ export interface Response {
|
|||||||
// From codersdk/roles.go
|
// From codersdk/roles.go
|
||||||
export interface Role {
|
export interface Role {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
|
readonly organization_id: string;
|
||||||
readonly display_name: string;
|
readonly display_name: string;
|
||||||
readonly site_permissions: readonly Permission[];
|
readonly site_permissions: readonly Permission[];
|
||||||
readonly organization_permissions: Record<string, readonly Permission[]>;
|
readonly organization_permissions: Record<string, readonly Permission[]>;
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export const MockOwnerRole: TypesGen.Role = {
|
|||||||
site_permissions: [],
|
site_permissions: [],
|
||||||
organization_permissions: {},
|
organization_permissions: {},
|
||||||
user_permissions: [],
|
user_permissions: [],
|
||||||
|
organization_id: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockUserAdminRole: TypesGen.Role = {
|
export const MockUserAdminRole: TypesGen.Role = {
|
||||||
@@ -243,6 +244,7 @@ export const MockUserAdminRole: TypesGen.Role = {
|
|||||||
site_permissions: [],
|
site_permissions: [],
|
||||||
organization_permissions: {},
|
organization_permissions: {},
|
||||||
user_permissions: [],
|
user_permissions: [],
|
||||||
|
organization_id: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockTemplateAdminRole: TypesGen.Role = {
|
export const MockTemplateAdminRole: TypesGen.Role = {
|
||||||
@@ -251,6 +253,7 @@ export const MockTemplateAdminRole: TypesGen.Role = {
|
|||||||
site_permissions: [],
|
site_permissions: [],
|
||||||
organization_permissions: {},
|
organization_permissions: {},
|
||||||
user_permissions: [],
|
user_permissions: [],
|
||||||
|
organization_id: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockMemberRole: TypesGen.SlimRole = {
|
export const MockMemberRole: TypesGen.SlimRole = {
|
||||||
@@ -264,6 +267,7 @@ export const MockAuditorRole: TypesGen.Role = {
|
|||||||
site_permissions: [],
|
site_permissions: [],
|
||||||
organization_permissions: {},
|
organization_permissions: {},
|
||||||
user_permissions: [],
|
user_permissions: [],
|
||||||
|
organization_id: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// assignableRole takes a role and a boolean. The boolean implies if the
|
// assignableRole takes a role and a boolean. The boolean implies if the
|
||||||
|
|||||||
Reference in New Issue
Block a user