diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index a6835014d4fe2..dd5205c0afb42 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -21,7 +21,8 @@ type Auditable interface { database.AuditOAuthConvertState | database.HealthSettings | database.OAuth2ProviderApp | - database.OAuth2ProviderAppSecret + database.OAuth2ProviderAppSecret | + database.CustomRole } // Map is a map of changed fields in an audited resource. It maps field names to diff --git a/coderd/audit/request.go b/coderd/audit/request.go index e6d9d01fbfd27..20eb8185af53e 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -103,6 +103,8 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.Name case database.OAuth2ProviderAppSecret: return typed.DisplaySecret + case database.CustomRole: + return typed.Name default: panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt)) } @@ -140,6 +142,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return typed.ID case database.OAuth2ProviderAppSecret: return typed.ID + case database.CustomRole: + return typed.ID default: panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt)) } @@ -175,6 +179,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeOauth2ProviderApp case database.OAuth2ProviderAppSecret: return database.ResourceTypeOauth2ProviderAppSecret + case database.CustomRole: + return database.ResourceTypeCustomRole default: panic(fmt.Sprintf("unknown resource %T for ResourceType", typed)) } @@ -211,6 +217,8 @@ func ResourceRequiresOrgID[T Auditable]() bool { return false case database.OAuth2ProviderAppSecret: return false + case database.CustomRole: + return true default: panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt)) } diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 316683a9f1e65..3ebca07686d0e 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -758,6 +758,8 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI roleName, _, err = rbac.RoleSplit(roleName) require.NoError(t, err, "split org role name") if ok { + roleName, _, err = rbac.RoleSplit(roleName) + require.NoError(t, err, "split rolename") orgRoles[orgID] = append(orgRoles[orgID], roleName) } else { siteRoles = append(siteRoles, roleName) diff --git a/coderd/database/dbauthz/customroles_test.go b/coderd/database/dbauthz/customroles_test.go index a5077121c0629..006f35d2a2fe9 100644 --- a/coderd/database/dbauthz/customroles_test.go +++ b/coderd/database/dbauthz/customroles_test.go @@ -38,7 +38,7 @@ func TestUpsertCustomRoles(t *testing.T) { Name: "can-assign", DisplayName: "", Site: rbac.Permissions(map[string][]policy.Action{ - rbac.ResourceAssignRole.Type: {policy.ActionCreate}, + rbac.ResourceAssignRole.Type: {policy.ActionRead, policy.ActionCreate}, }), } @@ -243,6 +243,19 @@ func TestUpsertCustomRoles(t *testing.T) { require.ErrorContains(t, err, tc.errorContains) } else { require.NoError(t, err) + + roles, err := az.CustomRoles(ctx, database.CustomRolesParams{ + LookupRoles: []database.NameOrganizationPair{ + { + Name: "test-role", + OrganizationID: tc.organizationID.UUID, + }, + }, + ExcludeOrgRoles: false, + OrganizationID: uuid.UUID{}, + }) + require.NoError(t, err) + require.Len(t, roles, 1) } }) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 2bfff39a949a9..70018e5ebd1a7 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1186,12 +1186,16 @@ func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesPar for _, role := range q.data.customRoles { role := role if len(arg.LookupRoles) > 0 { - if !slices.ContainsFunc(arg.LookupRoles, func(s string) bool { - roleName := rbac.RoleName(role.Name, "") - if role.OrganizationID.UUID != uuid.Nil { - roleName = rbac.RoleName(role.Name, role.OrganizationID.UUID.String()) + if !slices.ContainsFunc(arg.LookupRoles, func(s database.NameOrganizationPair) bool { + if !strings.EqualFold(s.Name, role.Name) { + return false } - return strings.EqualFold(s, roleName) + + if s.OrganizationID == uuid.Nil { + return true + } + + return s.OrganizationID == arg.OrganizationID }) { continue } @@ -8405,6 +8409,7 @@ func (q *FakeQuerier) UpsertCustomRole(_ context.Context, arg database.UpsertCus } role := database.CustomRole{ + ID: uuid.New(), Name: arg.Name, DisplayName: arg.DisplayName, OrganizationID: arg.OrganizationID, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 28adc8a36b1f1..c590d13c2862a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -142,7 +142,8 @@ CREATE TYPE resource_type AS ENUM ( 'convert_login', 'health_settings', 'oauth2_provider_app', - 'oauth2_provider_app_secret' + 'oauth2_provider_app_secret', + 'custom_role' ); CREATE TYPE startup_script_behavior AS ENUM ( @@ -412,7 +413,8 @@ CREATE TABLE custom_roles ( user_permissions jsonb DEFAULT '[]'::jsonb NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - organization_id uuid + organization_id uuid, + id uuid DEFAULT gen_random_uuid() NOT NULL ); COMMENT ON TABLE custom_roles IS 'Custom roles allow dynamic roles expanded at runtime'; @@ -1636,6 +1638,8 @@ CREATE INDEX idx_audit_log_user_id ON audit_logs USING btree (user_id); CREATE INDEX idx_audit_logs_time_desc ON audit_logs USING btree ("time" DESC); +CREATE INDEX idx_custom_roles_id ON custom_roles USING btree (id); + CREATE UNIQUE INDEX idx_custom_roles_name_lower ON custom_roles USING btree (lower(name)); CREATE INDEX idx_organization_member_organization_id_uuid ON organization_members USING btree (organization_id); diff --git a/coderd/database/migrations/000216_org_custom_role_audit.down.sql b/coderd/database/migrations/000216_org_custom_role_audit.down.sql new file mode 100644 index 0000000000000..5ad6106f2fc26 --- /dev/null +++ b/coderd/database/migrations/000216_org_custom_role_audit.down.sql @@ -0,0 +1,2 @@ +DROP INDEX idx_custom_roles_id; +ALTER TABLE custom_roles DROP COLUMN id; diff --git a/coderd/database/migrations/000216_org_custom_role_audit.up.sql b/coderd/database/migrations/000216_org_custom_role_audit.up.sql new file mode 100644 index 0000000000000..7a58bb3d7fa14 --- /dev/null +++ b/coderd/database/migrations/000216_org_custom_role_audit.up.sql @@ -0,0 +1,9 @@ +-- A role does not need to belong to an organization +ALTER TABLE custom_roles ALTER COLUMN organization_id DROP NOT NULL; + +-- (name) is the primary key, this column is almost exclusively for auditing. +ALTER TABLE custom_roles ADD COLUMN id uuid DEFAULT gen_random_uuid() NOT NULL; + +-- Ensure unique uuids. +CREATE INDEX idx_custom_roles_id ON custom_roles (id); +ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'custom_role'; diff --git a/coderd/database/models.go b/coderd/database/models.go index e5ba9fcea6841..0baa4a887c8f5 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 @@ -1222,6 +1222,7 @@ const ( ResourceTypeHealthSettings ResourceType = "health_settings" ResourceTypeOauth2ProviderApp ResourceType = "oauth2_provider_app" ResourceTypeOauth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" + ResourceTypeCustomRole ResourceType = "custom_role" ) func (e *ResourceType) Scan(src interface{}) error { @@ -1275,7 +1276,8 @@ func (e ResourceType) Valid() bool { ResourceTypeConvertLogin, ResourceTypeHealthSettings, ResourceTypeOauth2ProviderApp, - ResourceTypeOauth2ProviderAppSecret: + ResourceTypeOauth2ProviderAppSecret, + ResourceTypeCustomRole: return true } return false @@ -1298,6 +1300,7 @@ func AllResourceTypeValues() []ResourceType { ResourceTypeHealthSettings, ResourceTypeOauth2ProviderApp, ResourceTypeOauth2ProviderAppSecret, + ResourceTypeCustomRole, } } @@ -1792,6 +1795,7 @@ type CustomRole struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` // Roles can optionally be scoped to an organization OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` + ID uuid.UUID `db:"id" json:"id"` } // A table used to store the keys used to encrypt the database. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 6e2b1ff60cfdf..b9872c0ba13c4 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 677f972e734c3..722cb9eadbaed 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 @@ -5599,18 +5599,15 @@ func (q *sqlQuerier) UpdateReplica(ctx context.Context, arg UpdateReplicaParams) const customRoles = `-- name: CustomRoles :many SELECT - name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id + name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id FROM custom_roles WHERE true -- 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 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 []) + AND CASE WHEN array_length($1 :: name_organization_pair_list[], 1) > 0 THEN + (name, organization_id) ILIKE ANY ($1::name_organization_pair_list[]) ELSE true END -- Org scoping filter, to only fetch site wide roles @@ -5625,9 +5622,9 @@ WHERE ` type CustomRolesParams struct { - 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"` + LookupRoles []NameOrganizationPair `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) { @@ -5648,6 +5645,7 @@ func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([] &i.CreatedAt, &i.UpdatedAt, &i.OrganizationID, + &i.ID, ); err != nil { return nil, err } @@ -5692,7 +5690,7 @@ ON CONFLICT (name) 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 +RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id ` type UpsertCustomRoleParams struct { @@ -5723,6 +5721,7 @@ func (q *sqlQuerier) UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleP &i.CreatedAt, &i.UpdatedAt, &i.OrganizationID, + &i.ID, ) return i, err } diff --git a/coderd/database/queries/roles.sql b/coderd/database/queries/roles.sql index dd8816d40eecc..611bef287a5d7 100644 --- a/coderd/database/queries/roles.sql +++ b/coderd/database/queries/roles.sql @@ -7,11 +7,8 @@ WHERE true -- 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 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 []) + AND CASE WHEN array_length(@lookup_roles :: name_organization_pair_list[], 1) > 0 THEN + (name, organization_id) ILIKE ANY (@lookup_roles::name_organization_pair_list[]) ELSE true END -- Org scoping filter, to only fetch site wide roles diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index ff8faf5f7704c..093a5798953d6 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -28,6 +28,9 @@ sql: emit_enum_valid_method: true emit_all_enum_values: true overrides: + - db_type: "name_organization_pair_list" + go_type: + type: "NameOrganizationPair" - column: "custom_roles.site_permissions" go_type: type: "CustomRolePermissions" diff --git a/coderd/database/types.go b/coderd/database/types.go index 5d0490d0c9020..70f37fdc08b9d 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -113,6 +113,26 @@ func (m StringMapOfInt) Value() (driver.Value, error) { return json.Marshal(m) } +// NameOrganizationPair is used as a lookup tuple for custom role rows. +type NameOrganizationPair struct { + Name string `db:"name" json:"name"` + // OrganizationID if unset will assume a null column value + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` +} + +func (a *NameOrganizationPair) Scan(src interface{}) error { + return xerrors.Errorf("unexpected type %T", src) +} + +func (a NameOrganizationPair) Value() (driver.Value, error) { + var orgID interface{} = a.OrganizationID + if a.OrganizationID == uuid.Nil { + orgID = nil + } + + return []interface{}{a.Name, orgID}, nil +} + type CustomRolePermissions []CustomRolePermission func (a *CustomRolePermissions) Scan(src interface{}) error { diff --git a/coderd/rbac/rolestore/rolestore.go b/coderd/rbac/rolestore/rolestore.go index 083f03877aa83..80cbd1165073b 100644 --- a/coderd/rbac/rolestore/rolestore.go +++ b/coderd/rbac/rolestore/rolestore.go @@ -69,11 +69,34 @@ func Expand(ctx context.Context, db database.Store, names []string) (rbac.Roles, } if len(lookup) > 0 { + // The set of roles coming in are formatted as 'rolename[:]'. + // In the database, org roles are scoped with an organization column. + lookupArgs := make([]database.NameOrganizationPair, 0, len(lookup)) + for _, name := range lookup { + roleName, orgID, err := rbac.RoleSplit(name) + if err != nil { + continue + } + + parsedOrgID := uuid.Nil // Default to no org ID + if orgID != "" { + parsedOrgID, err = uuid.Parse(orgID) + if err != nil { + continue + } + } + + lookupArgs = append(lookupArgs, database.NameOrganizationPair{ + Name: roleName, + OrganizationID: parsedOrgID, + }) + } + // If some roles are missing from the database, they are omitted from // the expansion. These roles are no-ops. Should we raise some kind of // warning when this happens? dbroles, err := db.CustomRoles(ctx, database.CustomRolesParams{ - LookupRoles: lookup, + LookupRoles: lookupArgs, ExcludeOrgRoles: false, OrganizationID: uuid.Nil, }) diff --git a/coderd/roles.go b/coderd/roles.go index 94b121940ed45..1e7f1b1473b9a 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -20,12 +20,12 @@ import ( // roles. Ideally only included in the enterprise package, but the routes are // intermixed with AGPL endpoints. type CustomRoleHandler interface { - PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) + PatchOrganizationRole(ctx context.Context, rw http.ResponseWriter, r *http.Request, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) } type agplCustomRoleHandler struct{} -func (agplCustomRoleHandler) PatchOrganizationRole(ctx context.Context, _ database.Store, rw http.ResponseWriter, _ uuid.UUID, _ codersdk.Role) (codersdk.Role, bool) { +func (agplCustomRoleHandler) PatchOrganizationRole(ctx context.Context, rw http.ResponseWriter, _ *http.Request, _ uuid.UUID, _ codersdk.Role) (codersdk.Role, bool) { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "Creating and updating custom roles is an Enterprise feature. Contact sales!", }) @@ -54,7 +54,7 @@ func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) { return } - updated, ok := handler.PatchOrganizationRole(ctx, api.Database, rw, organization.ID, req) + updated, ok := handler.PatchOrganizationRole(ctx, rw, r, organization.ID, req) if !ok { return } diff --git a/codersdk/audit.go b/codersdk/audit.go index 553bd9cc2dbea..837ef729e4a58 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -30,6 +30,7 @@ const ( ResourceTypeOAuth2ProviderApp ResourceType = "oauth2_provider_app" // nolint:gosec // This is not a secret. ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" + ResourceTypeCustomRole ResourceType = "custom_role" ) func (r ResourceType) FriendlyString() string { @@ -66,6 +67,8 @@ func (r ResourceType) FriendlyString() string { return "oauth2 app" case ResourceTypeOAuth2ProviderAppSecret: return "oauth2 app secret" + case ResourceTypeCustomRole: + return "custom role" default: return "unknown" } diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go index 59780d2918418..5a24c93c4991c 100644 --- a/enterprise/audit/diff.go +++ b/enterprise/audit/diff.go @@ -144,6 +144,19 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) { return leftInt64Ptr, rightInt64Ptr, true case database.TemplateACL: return fmt.Sprintf("%+v", left), fmt.Sprintf("%+v", right), true + case database.CustomRolePermissions: + leftArr := make([]string, 0) + rightArr := make([]string, 0) + for _, p := range typedLeft { + leftArr = append(leftArr, p.String()) + } + for _, p := range right.(database.CustomRolePermissions) { + rightArr = append(rightArr, p.String()) + } + + // String representation is much easier to visually inspect + //typedRight := right.(database.CustomRolePermission) + return leftArr, rightArr, true default: return left, right, false } diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index f26b4921aaace..e2788959e3275 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -50,6 +50,18 @@ type Table map[string]map[string]Action var AuditableResources = auditMap(auditableResourcesTypes) var auditableResourcesTypes = map[any]map[string]Action{ + &database.CustomRole{}: { + "name": ActionTrack, + "display_name": ActionTrack, + "site_permissions": ActionTrack, + "org_permissions": ActionTrack, + "user_permissions": ActionTrack, + "organization_id": ActionTrack, + + "id": ActionIgnore, + "created_at": ActionIgnore, + "updated_at": ActionIgnore, + }, &database.GitSSHKey{}: { "user_id": ActionTrack, "created_at": ActionIgnore, // Never changes, but is implicit and not helpful in a diff. diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 574d2c12dd2de..26fdab6ec1bfb 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -746,7 +746,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { } if initial, changed, enabled := featureChanged(codersdk.FeatureCustomRoles); shouldUpdate(initial, changed, enabled) { - var handler coderd.CustomRoleHandler = &enterpriseCustomRoleHandler{Enabled: enabled} + var handler coderd.CustomRoleHandler = &enterpriseCustomRoleHandler{API: api, Enabled: enabled} api.AGPL.CustomRoleHandler.Store(&handler) } diff --git a/enterprise/coderd/roles.go b/enterprise/coderd/roles.go index b3a24a8a7779f..3cac9d8c2303e 100644 --- a/enterprise/coderd/roles.go +++ b/enterprise/coderd/roles.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/httpapi" @@ -15,10 +16,11 @@ import ( ) type enterpriseCustomRoleHandler struct { + API *API Enabled bool } -func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, db database.Store, rw http.ResponseWriter, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) { +func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, rw http.ResponseWriter, r *http.Request, orgID uuid.UUID, role codersdk.Role) (codersdk.Role, bool) { if !h.Enabled { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "Custom roles are not enabled", @@ -26,6 +28,19 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, return codersdk.Role{}, false } + var ( + db = h.API.Database + auditor = h.API.AGPL.Auditor.Load() + aReq, commitAudit = audit.InitRequest[database.CustomRole](rw, &audit.RequestParams{ + Audit: *auditor, + Log: h.API.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: orgID, + }) + ) + defer commitAudit() + if err := httpapi.NameValid(role.Name); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid role name", @@ -59,6 +74,21 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, return codersdk.Role{}, false } + originalRoles, err := db.CustomRoles(ctx, database.CustomRolesParams{ + LookupRoles: []string{role.Name}, + ExcludeOrgRoles: false, + OrganizationID: orgID, + }) + // If it is a 404 (not found) error, ignore it. + if err != nil && !httpapi.Is404Error(err) { + httpapi.InternalServerError(rw, err) + return codersdk.Role{}, false + } + if len(originalRoles) == 1 { + // For auditing changes to a role. + aReq.Old = originalRoles[1] + } + inserted, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{ Name: role.Name, DisplayName: role.DisplayName, @@ -81,6 +111,7 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context, }) return codersdk.Role{}, false } + aReq.New = inserted return db2sdk.Role(inserted), true } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b7de0be88ba37..1000818375fce 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2174,6 +2174,7 @@ export const RBACResources: RBACResource[] = [ export type ResourceType = | "api_key" | "convert_login" + | "custom_role" | "git_ssh_key" | "group" | "health_settings" @@ -2190,6 +2191,7 @@ export type ResourceType = export const ResourceTypes: ResourceType[] = [ "api_key", "convert_login", + "custom_role", "git_ssh_key", "group", "health_settings",