From a79bb89e22d14253a53293ee7dbdd062d500868e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 11:06:49 -1000 Subject: [PATCH 1/8] chore: include custom roles in list org roles --- coderd/apidoc/docs.go | 8 +++++++ coderd/apidoc/swagger.json | 8 +++++++ coderd/database/db2sdk/db2sdk.go | 11 ++++++--- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 24 ++++++++++++++------ coderd/database/queries/roles.sql | 7 ++++++ coderd/rbac/roles.go | 36 +++++++++++++++--------------- coderd/rbac/rolestore/rolestore.go | 25 +++++++++++++++++++-- coderd/roles.go | 20 ++++++++++++++++- codersdk/roles.go | 3 ++- docs/api/members.md | 6 +++++ docs/api/schemas.md | 4 ++++ site/src/api/typesGenerated.ts | 1 + 14 files changed, 123 insertions(+), 34 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 34c4c6b529d19..22a95e3ce4b54 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8421,6 +8421,10 @@ const docTemplate = `{ "name": { "type": "string" }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "organization_permissions": { "description": "map[\u003corg_id\u003e] -\u003e Permissions", "type": "object", @@ -11241,6 +11245,10 @@ const docTemplate = `{ "name": { "type": "string" }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "organization_permissions": { "description": "map[\u003corg_id\u003e] -\u003e Permissions", "type": "object", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 43aacb5e0cc32..695993c6b427b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7476,6 +7476,10 @@ "name": { "type": "string" }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "organization_permissions": { "description": "map[\u003corg_id\u003e] -\u003e Permissions", "type": "object", @@ -10133,6 +10137,10 @@ "name": { "type": "string" }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "organization_permissions": { "description": "map[\u003corg_id\u003e] -\u003e Permissions", "type": "object", diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index ab6f3aa82b3f6..590183bd43dd1 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -527,12 +527,17 @@ func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.Provisioner } func Role(role rbac.Role) codersdk.Role { + roleName, orgIDStr, err := rbac.RoleSplit(role.Name) + if err != nil { + roleName = role.Name + } return codersdk.Role{ - Name: role.Name, + Name: roleName, + OrganizationID: orgIDStr, DisplayName: role.DisplayName, SitePermissions: List(role.Site, 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 { return rbac.Role{ - Name: role.Name, + Name: rbac.RoleName(role.Name, role.OrganizationID), DisplayName: role.DisplayName, Site: List(role.SitePermissions, PermissionToRBAC), Org: Map(role.OrganizationPermissions, ListLazy(PermissionToRBAC)), diff --git a/coderd/database/models.go b/coderd/database/models.go index 42c41c83bd5dc..e8934fc12678e 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index a590ae87bc8fd..52ec03194b6a1 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8f5a879d75f5c..9f86f80de0fff 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.25.0 +// sqlc v1.26.0 package database @@ -5615,15 +5615,21 @@ WHERE organization_id IS null ELSE true END + -- Org scoping filter, to only fetch site wide roles + AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + organization_id = $3 + ELSE true + END ` type CustomRolesParams struct { - LookupRoles []string `db:"lookup_roles" json:"lookup_roles"` - ExcludeOrgRoles bool `db:"exclude_org_roles" json:"exclude_org_roles"` + LookupRoles []string `db:"lookup_roles" json:"lookup_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) { - 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 { return nil, err } @@ -5659,6 +5665,7 @@ INSERT INTO custom_roles ( name, display_name, + organization_id, site_permissions, org_permissions, user_permissions, @@ -5672,15 +5679,16 @@ VALUES ( $3, $4, $5, + $6, now(), now() ) ON CONFLICT (name) DO UPDATE SET display_name = $2, - site_permissions = $3, - org_permissions = $4, - user_permissions = $5, + site_permissions = $4, + org_permissions = $5, + user_permissions = $6, updated_at = now() RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id ` @@ -5688,6 +5696,7 @@ RETURNING name, display_name, site_permissions, org_permissions, user_permission type UpsertCustomRoleParams struct { Name string `db:"name" json:"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"` OrgPermissions json.RawMessage `db:"org_permissions" json:"org_permissions"` UserPermissions json.RawMessage `db:"user_permissions" json:"user_permissions"` @@ -5697,6 +5706,7 @@ func (q *sqlQuerier) UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleP row := q.db.QueryRowContext(ctx, upsertCustomRole, arg.Name, arg.DisplayName, + arg.OrganizationID, arg.SitePermissions, arg.OrgPermissions, arg.UserPermissions, diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index 2137dea34b077..df14c385003eb 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -16,6 +16,11 @@ WHERE organization_id IS null ELSE true END + -- Org scoping filter, to only fetch site wide roles + AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + organization_id = @organization_id + ELSE true + END ; -- name: UpsertCustomRole :one @@ -23,6 +28,7 @@ INSERT INTO custom_roles ( name, display_name, + organization_id, site_permissions, org_permissions, user_permissions, @@ -33,6 +39,7 @@ VALUES ( -- Always force lowercase names lower(@name), @display_name, + @organization_id, @site_permissions, @org_permissions, @user_permissions, diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 7086e2fe0e2a4..137d2c0c1258b 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -53,29 +53,29 @@ func (names RoleNames) Names() []string { // site and orgs, and these functions can be removed. func RoleOwner() string { - return roleName(owner, "") + return RoleName(owner, "") } -func CustomSiteRole() string { return roleName(customSiteRole, "") } +func CustomSiteRole() string { return RoleName(customSiteRole, "") } func RoleTemplateAdmin() string { - return roleName(templateAdmin, "") + return RoleName(templateAdmin, "") } func RoleUserAdmin() string { - return roleName(userAdmin, "") + return RoleName(userAdmin, "") } func RoleMember() string { - return roleName(member, "") + return RoleName(member, "") } func RoleOrgAdmin(organizationID uuid.UUID) string { - return roleName(orgAdmin, organizationID.String()) + return RoleName(orgAdmin, organizationID.String()) } func RoleOrgMember(organizationID uuid.UUID) string { - return roleName(orgMember, organizationID.String()) + return RoleName(orgMember, organizationID.String()) } func allPermsExcept(excepts ...Objecter) []Permission { @@ -273,7 +273,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // organization scope. orgAdmin: func(organizationID string) Role { return Role{ - Name: roleName(orgAdmin, organizationID), + Name: RoleName(orgAdmin, organizationID), DisplayName: "Organization Admin", Site: []Permission{}, Org: map[string][]Permission{ @@ -291,7 +291,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // in an organization. orgMember: func(organizationID string) Role { return Role{ - Name: roleName(orgMember, organizationID), + Name: RoleName(orgMember, organizationID), DisplayName: "", Site: []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. roles := expandable.Names() - assigned, assignedOrg, err := roleSplit(assignedRole) + assigned, assignedOrg, err := RoleSplit(assignedRole) if err != nil { return false } for _, longRole := range roles { - role, orgID, err := roleSplit(longRole) + role, orgID, err := RoleSplit(longRole) if err != nil { continue } @@ -510,7 +510,7 @@ func CanAssignRole(expandable ExpandableRoles, assignedRole string) bool { // api. We should maybe make an exported function that returns just the // human-readable content of the Role struct (name + display name). func RoleByName(name string) (Role, error) { - roleName, orgID, err := roleSplit(name) + roleName, orgID, err := RoleSplit(name) if err != nil { 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) { - _, orgID, err := roleSplit(roleName) + _, orgID, err := RoleSplit(roleName) if err == nil && orgID != "" { return orgID, true } @@ -561,7 +561,7 @@ func OrganizationRoles(organizationID uuid.UUID) []Role { var roles []Role for _, roleF := range builtInRoles { role := roleF(organizationID.String()) - _, scope, err := roleSplit(role.Name) + _, scope, err := RoleSplit(role.Name) if err != nil { // This should never happen continue @@ -582,7 +582,7 @@ func SiteRoles() []Role { var roles []Role for _, roleF := range builtInRoles { role := roleF("random") - _, scope, err := roleSplit(role.Name) + _, scope, err := RoleSplit(role.Name) if err != nil { // This should never happen continue @@ -625,19 +625,19 @@ func ChangeRoleSet(from []string, to []string) (added []string, removed []string return added, removed } -// roleName is a quick helper function to return +// RoleName is a quick helper function to return // // role_name:scopeID // // 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 == "" { return name } 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, ":") if len(arr) > 2 { return "", "", xerrors.Errorf("too many colons in role name") diff --git a/coderd/rbac/rolestore/rolestore.go b/coderd/rbac/rolestore/rolestore.go index 9881cde028826..b1957fb3b25e9 100644 --- a/coderd/rbac/rolestore/rolestore.go +++ b/coderd/rbac/rolestore/rolestore.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" + "github.com/google/uuid" "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" @@ -95,8 +96,12 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles, } 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{ - Name: dbRole.Name, + Name: name, DisplayName: dbRole.DisplayName, Site: nil, Org: nil, @@ -122,11 +127,27 @@ func ConvertDBRole(dbRole database.CustomRole) (rbac.Role, 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{ - Name: role.Name, + Name: roleName, 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) if err != nil { return dbRole, xerrors.Errorf("marshal site permissions: %w", err) diff --git a/coderd/roles.go b/coderd/roles.go index 3d6245f9d4594..813faa643a164 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -73,7 +73,25 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { } 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 { diff --git a/codersdk/roles.go b/codersdk/roles.go index 29b0174931fbe..c803e92f44bb2 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -35,7 +35,8 @@ type Permission struct { // Role is a longer form of SlimRole used to edit custom roles. 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"` SitePermissions []Permission `json:"site_permissions" table:"site_permissions"` // map[] -> Permissions diff --git a/docs/api/members.md b/docs/api/members.md index 8b34200e50e95..27536a6c836fa 100644 --- a/docs/api/members.md +++ b/docs/api/members.md @@ -30,6 +30,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members "built_in": true, "display_name": "string", "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_permissions": { "property1": [ { @@ -81,6 +82,7 @@ Status Code **200** | `» built_in` | boolean | false | | Built in roles are immutable | | `» display_name` | string | false | | | | `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | | `» organization_permissions` | object | false | | map[] -> Permissions | | `»» [any property]` | array | 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, "display_name": "string", "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_permissions": { "property1": [ { @@ -266,6 +269,7 @@ Status Code **200** | `» built_in` | boolean | false | | Built in roles are immutable | | `» display_name` | string | false | | | | `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | | `» organization_permissions` | object | false | | map[] -> Permissions | | `»» [any property]` | array | 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", "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_permissions": { "property1": [ { @@ -390,6 +395,7 @@ Status Code **200** | `[array item]` | array | false | | | | `» display_name` | string | false | | | | `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | | `» organization_permissions` | object | false | | map[] -> Permissions | | `»» [any property]` | array | false | | | | `»»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | | diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 67fb461ee1b0b..40e1d1319a22a 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -805,6 +805,7 @@ "built_in": true, "display_name": "string", "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_permissions": { "property1": [ { @@ -846,6 +847,7 @@ | `built_in` | boolean | false | | Built in roles are immutable | | `display_name` | string | false | | | | `name` | string | false | | | +| `organization_id` | string | false | | | | `organization_permissions` | object | false | | map[] -> Permissions | | » `[any property]` | 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", "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_permissions": { "property1": [ { @@ -4366,6 +4369,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | -------------------------- | --------------------------------------------------- | -------- | ------------ | ---------------------------- | | `display_name` | string | false | | | | `name` | string | false | | | +| `organization_id` | string | false | | | | `organization_permissions` | object | false | | map[] -> Permissions | | » `[any property]` | array of [codersdk.Permission](#codersdkpermission) | false | | | | `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index db1b39fdbed26..5d4d148758f36 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -977,6 +977,7 @@ export interface Response { // From codersdk/roles.go export interface Role { readonly name: string; + readonly organization_id: string; readonly display_name: string; readonly site_permissions: readonly Permission[]; readonly organization_permissions: Record; From d6c0d2ba6413aa4aaaf1f51b3473ca6731aa8866 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 11:30:25 -1000 Subject: [PATCH 2/8] Add unit test --- coderd/database/dbgen/dbgen.go | 14 ++++++++++++ coderd/database/dbmem/dbmem.go | 6 ++++++ coderd/roles_test.go | 39 ++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 93d629e71e49f..be612abc333f9 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "net" + "strings" "testing" "time" @@ -817,6 +818,19 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth 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 { if err != nil { panic(err) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 5f2ebbff25003..edd033aa54564 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1197,6 +1197,10 @@ func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesPar continue } + if arg.OrganizationID != uuid.Nil && role.OrganizationID.UUID != arg.OrganizationID { + continue + } + found = append(found, role) } @@ -8377,6 +8381,7 @@ func (q *FakeQuerier) UpsertCustomRole(_ context.Context, arg database.UpsertCus for i := range q.customRoles { if strings.EqualFold(q.customRoles[i].Name, arg.Name) { q.customRoles[i].DisplayName = arg.DisplayName + q.customRoles[i].OrganizationID = arg.OrganizationID q.customRoles[i].SitePermissions = arg.SitePermissions q.customRoles[i].OrgPermissions = arg.OrgPermissions q.customRoles[i].UserPermissions = arg.UserPermissions @@ -8388,6 +8393,7 @@ func (q *FakeQuerier) UpsertCustomRole(_ context.Context, arg database.UpsertCus role := database.CustomRole{ Name: arg.Name, DisplayName: arg.DisplayName, + OrganizationID: arg.OrganizationID, SitePermissions: arg.SitePermissions, OrgPermissions: arg.OrgPermissions, UserPermissions: arg.UserPermissions, diff --git a/coderd/roles_test.go b/coderd/roles_test.go index d82c03033cb54..721ae9de29658 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -3,13 +3,17 @@ package coderd_test import ( "context" "net/http" + "slices" "testing" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" "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/policy" + "github.com/coder/coder/v2/coderd/rbac/rolestore" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -156,6 +160,41 @@ func TestListRoles(t *testing.T) { } } +func TestListCustomRoles(t *testing.T) { + t.Parallel() + + t.Run("Organizations", func(t *testing.T) { + 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 { role, _ := rbac.RoleByName(roleName) return db2sdk.Role(role) From 95a8931fc8e763bc39e38088d57c167311874f1e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 11:33:57 -1000 Subject: [PATCH 3/8] cleanup --- coderd/database/models.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 7 +++---- coderd/database/queries/roles.sql | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index e8934fc12678e..42c41c83bd5dc 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 52ec03194b6a1..a590ae87bc8fd 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9f86f80de0fff..8c3cc8d11b13a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.25.0 package database @@ -2983,7 +2983,7 @@ func (q *sqlQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, } const upsertJFrogXrayScanByWorkspaceAndAgentID = `-- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec -INSERT INTO +INSERT INTO jfrog_xray_scans ( agent_id, workspace_id, @@ -2992,7 +2992,7 @@ INSERT INTO medium, results_url ) -VALUES +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (agent_id, workspace_id) DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6 @@ -5615,7 +5615,6 @@ WHERE organization_id IS null ELSE true END - -- Org scoping filter, to only fetch site wide roles AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN organization_id = $3 ELSE true diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index df14c385003eb..56d7907e9c80d 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -16,7 +16,6 @@ WHERE organization_id IS null ELSE true END - -- Org scoping filter, to only fetch site wide roles AND CASE WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN organization_id = @organization_id ELSE true From f2c7b60e523d777e899fe69fda8c2b6282f4ec47 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 11:49:43 -1000 Subject: [PATCH 4/8] move cli show roles to org scope --- cli/organization.go | 1 + .../rolescmd.go => cli/organizationroles.go | 18 ++--- cli/organizationroles_test.go | 50 ++++++++++++++ enterprise/cli/rolescmd_test.go | 68 ------------------- enterprise/cli/root.go | 1 - 5 files changed, 61 insertions(+), 77 deletions(-) rename enterprise/cli/rolescmd.go => cli/organizationroles.go (89%) create mode 100644 cli/organizationroles_test.go delete mode 100644 enterprise/cli/rolescmd_test.go diff --git a/cli/organization.go b/cli/organization.go index d9ea5c7aaf4ac..beb52cb5df8f2 100644 --- a/cli/organization.go +++ b/cli/organization.go @@ -30,6 +30,7 @@ func (r *RootCmd) organizations() *serpent.Command { r.currentOrganization(), r.switchOrganization(), r.createOrganization(), + r.organizationRoles(), }, } diff --git a/enterprise/cli/rolescmd.go b/cli/organizationroles.go similarity index 89% rename from enterprise/cli/rolescmd.go rename to cli/organizationroles.go index b0a9346697a01..91d1b20f54dd4 100644 --- a/enterprise/cli/rolescmd.go +++ b/cli/organizationroles.go @@ -12,26 +12,23 @@ import ( "github.com/coder/serpent" ) -// **NOTE** Only covers site wide roles at present. Org scoped roles maybe -// should be nested under some command that scopes to an org?? - -func (r *RootCmd) roles() *serpent.Command { +func (r *RootCmd) organizationRoles() *serpent.Command { cmd := &serpent.Command{ Use: "roles", - Short: "Manage site-wide roles.", + Short: "Manage organization roles.", Aliases: []string{"role"}, Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) }, Hidden: true, Children: []*serpent.Command{ - r.showRole(), + r.showOrganizationRoles(), }, } return cmd } -func (r *RootCmd) showRole() *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"}), @@ -67,7 +64,12 @@ func (r *RootCmd) showRole() *serpent.Command { ), Handler: func(inv *serpent.Invocation) error { 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 { return xerrors.Errorf("listing roles: %w", err) } diff --git a/cli/organizationroles_test.go b/cli/organizationroles_test.go new file mode 100644 index 0000000000000..4cfaf5d2f179e --- /dev/null +++ b/cli/organizationroles_test.go @@ -0,0 +1,50 @@ +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/testutil" +) + +func TestShowOrganizationRoles(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{}) + owner := coderdtest.CreateFirstUser(t, client) + + 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, + }, + }) + + // Requires an owner + 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) + }) +} diff --git a/enterprise/cli/rolescmd_test.go b/enterprise/cli/rolescmd_test.go deleted file mode 100644 index df776603e0ac4..0000000000000 --- a/enterprise/cli/rolescmd_test.go +++ /dev/null @@ -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) - } - }) -} diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go index 69b686c4174aa..74615ff0e9d2e 100644 --- a/enterprise/cli/root.go +++ b/enterprise/cli/root.go @@ -17,7 +17,6 @@ func (r *RootCmd) enterpriseOnly() []*serpent.Command { r.licenses(), r.groups(), r.provisionerDaemons(), - r.roles(), } } From 74729af52707af61e996a65a004115d43c064729 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 11:58:01 -1000 Subject: [PATCH 5/8] fix ts mockups --- site/src/testHelpers/entities.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 22a4c5db6edd9..1fbb18aa86a07 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -235,6 +235,7 @@ export const MockOwnerRole: TypesGen.Role = { site_permissions: [], organization_permissions: {}, user_permissions: [], + organization_id: "", }; export const MockUserAdminRole: TypesGen.Role = { @@ -243,6 +244,7 @@ export const MockUserAdminRole: TypesGen.Role = { site_permissions: [], organization_permissions: {}, user_permissions: [], + organization_id: "", }; export const MockTemplateAdminRole: TypesGen.Role = { @@ -251,6 +253,7 @@ export const MockTemplateAdminRole: TypesGen.Role = { site_permissions: [], organization_permissions: {}, user_permissions: [], + organization_id: "", }; export const MockMemberRole: TypesGen.SlimRole = { @@ -264,6 +267,7 @@ export const MockAuditorRole: TypesGen.Role = { site_permissions: [], organization_permissions: {}, user_permissions: [], + organization_id: "", }; // assignableRole takes a role and a boolean. The boolean implies if the From 520d32e6a2ba94b106b065677437c74423fb8f59 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 11:58:28 -1000 Subject: [PATCH 6/8] make gne --- coderd/database/queries.sql.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8c3cc8d11b13a..f6e823e567a03 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2983,7 +2983,7 @@ func (q *sqlQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, } const upsertJFrogXrayScanByWorkspaceAndAgentID = `-- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec -INSERT INTO +INSERT INTO jfrog_xray_scans ( agent_id, workspace_id, @@ -2992,7 +2992,7 @@ INSERT INTO medium, results_url ) -VALUES +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (agent_id, workspace_id) DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6 From 52835d5fc7eaa1f65bd0fc90cba64a130796fc47 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 12:19:15 -1000 Subject: [PATCH 7/8] chore: rolestore to handle custom org roles --- cli/organizationroles_test.go | 7 +++-- coderd/database/dbmem/dbmem.go | 6 +++- coderd/database/queries.sql.go | 9 ++++-- coderd/database/queries/roles.sql | 9 ++++-- coderd/rbac/rolestore/rolestore.go | 1 + coderd/rbac/rolestore/rolestore_test.go | 41 +++++++++++++++++++++++++ coderd/roles.go | 5 ++- coderd/roles_test.go | 2 ++ 8 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 coderd/rbac/rolestore/rolestore_test.go diff --git a/cli/organizationroles_test.go b/cli/organizationroles_test.go index 4cfaf5d2f179e..d96c38c4bb9d6 100644 --- a/cli/organizationroles_test.go +++ b/cli/organizationroles_test.go @@ -11,6 +11,7 @@ import ( "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" ) @@ -20,8 +21,9 @@ func TestShowOrganizationRoles(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() - client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{}) - owner := coderdtest.CreateFirstUser(t, client) + 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{ @@ -36,7 +38,6 @@ func TestShowOrganizationRoles(t *testing.T) { }, }) - // Requires an owner ctx := testutil.Context(t, testutil.WaitMedium) inv, root := clitest.New(t, "organization", "roles", "show") clitest.SetupConfig(t, client, root) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index edd033aa54564..e9497880b274c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1187,7 +1187,11 @@ func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesPar role := role if len(arg.LookupRoles) > 0 { 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 } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f6e823e567a03..bcc961c88e048 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5604,10 +5604,13 @@ FROM custom_roles WHERE true - -- Lookup roles filter + -- Lookup roles filter expects the role names to be in the rbac package + -- format. Eg: name[:] AND CASE WHEN array_length($1 :: text[], 1) > 0 THEN - -- Case insensitive - name ILIKE ANY($1 :: text []) + -- Case insensitive lookup with org_id appended (if non-null). + -- 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 END -- Org scoping filter, to only fetch site wide roles diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index 56d7907e9c80d..dd8816d40eecc 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -5,10 +5,13 @@ FROM custom_roles WHERE true - -- Lookup roles filter + -- Lookup roles filter expects the role names to be in the rbac package + -- format. Eg: name[:] AND CASE WHEN array_length(@lookup_roles :: text[], 1) > 0 THEN - -- Case insensitive - name ILIKE ANY(@lookup_roles :: text []) + -- Case insensitive lookup with org_id appended (if non-null). + -- 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 END -- Org scoping filter, to only fetch site wide roles diff --git a/coderd/rbac/rolestore/rolestore.go b/coderd/rbac/rolestore/rolestore.go index b1957fb3b25e9..e0d199241fc9f 100644 --- a/coderd/rbac/rolestore/rolestore.go +++ b/coderd/rbac/rolestore/rolestore.go @@ -76,6 +76,7 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles, dbroles, err := db.CustomRoles(ctx, database.CustomRolesParams{ LookupRoles: lookup, ExcludeOrgRoles: false, + OrganizationID: uuid.Nil, }) if err != nil { return nil, xerrors.Errorf("fetch custom roles: %w", err) diff --git a/coderd/rbac/rolestore/rolestore_test.go b/coderd/rbac/rolestore/rolestore_test.go new file mode 100644 index 0000000000000..318f2f579b340 --- /dev/null +++ b/coderd/rbac/rolestore/rolestore_test.go @@ -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") +} diff --git a/coderd/roles.go b/coderd/roles.go index 813faa643a164..a00af23ce98eb 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -3,6 +3,8 @@ package coderd import ( "net/http" + "github.com/google/uuid" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "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{ + LookupRoles: nil, // Only site wide custom roles to be included ExcludeOrgRoles: true, - LookupRoles: nil, + OrganizationID: uuid.Nil, }) if err != nil { httpapi.InternalServerError(rw, err) diff --git a/coderd/roles_test.go b/coderd/roles_test.go index 721ae9de29658..6d4f4bb6fe789 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -164,6 +164,8 @@ 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) From a0e5aef4e9b763497d7e81d2f65884331dc05142 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 21 May 2024 12:31:36 -1000 Subject: [PATCH 8/8] Lint --- enterprise/coderd/roles.go | 3 +++ enterprise/coderd/roles_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index 552197f7c4401..8e0827c9b3b02 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -3,6 +3,8 @@ package coderd import ( "net/http" + "github.com/google/uuid" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "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{ Name: args.Name, DisplayName: args.DisplayName, + OrganizationID: uuid.NullUUID{}, SitePermissions: args.SitePermissions, OrgPermissions: args.OrgPermissions, UserPermissions: args.UserPermissions, diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 67b863e63bacd..a7db9b718d946 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -198,6 +198,6 @@ func TestCustomRole(t *testing.T) { OrganizationPermissions: nil, UserPermissions: nil, }) - require.ErrorContains(t, err, "Invalid role name") + require.ErrorContains(t, err, "Validation") }) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy