mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: implement audit log for custom role edits (#13494)
* chore: implement audit log for custom role edits
This commit is contained in:
Generated
+4
-2
@@ -11330,7 +11330,8 @@ const docTemplate = `{
|
||||
"workspace_proxy",
|
||||
"organization",
|
||||
"oauth2_provider_app",
|
||||
"oauth2_provider_app_secret"
|
||||
"oauth2_provider_app_secret",
|
||||
"custom_role"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ResourceTypeTemplate",
|
||||
@@ -11347,7 +11348,8 @@ const docTemplate = `{
|
||||
"ResourceTypeWorkspaceProxy",
|
||||
"ResourceTypeOrganization",
|
||||
"ResourceTypeOAuth2ProviderApp",
|
||||
"ResourceTypeOAuth2ProviderAppSecret"
|
||||
"ResourceTypeOAuth2ProviderAppSecret",
|
||||
"ResourceTypeCustomRole"
|
||||
]
|
||||
},
|
||||
"codersdk.Response": {
|
||||
|
||||
Generated
+4
-2
@@ -10219,7 +10219,8 @@
|
||||
"workspace_proxy",
|
||||
"organization",
|
||||
"oauth2_provider_app",
|
||||
"oauth2_provider_app_secret"
|
||||
"oauth2_provider_app_secret",
|
||||
"custom_role"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ResourceTypeTemplate",
|
||||
@@ -10236,7 +10237,8 @@
|
||||
"ResourceTypeWorkspaceProxy",
|
||||
"ResourceTypeOrganization",
|
||||
"ResourceTypeOAuth2ProviderApp",
|
||||
"ResourceTypeOAuth2ProviderAppSecret"
|
||||
"ResourceTypeOAuth2ProviderAppSecret",
|
||||
"ResourceTypeCustomRole"
|
||||
]
|
||||
},
|
||||
"codersdk.Response": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -244,7 +244,7 @@ func TestUpsertCustomRoles(t *testing.T) {
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify we can fetch the role
|
||||
// Verify the role is fetched with the lookup filter.
|
||||
roles, err := az.CustomRoles(ctx, database.CustomRolesParams{
|
||||
LookupRoles: []database.NameOrganizationPair{
|
||||
{
|
||||
|
||||
@@ -8415,6 +8415,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,
|
||||
|
||||
Generated
+8
-2
@@ -147,7 +147,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 (
|
||||
@@ -417,13 +418,16 @@ 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';
|
||||
|
||||
COMMENT ON COLUMN custom_roles.organization_id IS 'Roles can optionally be scoped to an organization';
|
||||
|
||||
COMMENT ON COLUMN custom_roles.id IS 'Custom roles ID is used purely for auditing purposes. Name is a better unique identifier.';
|
||||
|
||||
CREATE TABLE dbcrypt_keys (
|
||||
number integer NOT NULL,
|
||||
active_key_digest text,
|
||||
@@ -1642,6 +1646,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);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP INDEX idx_custom_roles_id;
|
||||
ALTER TABLE custom_roles DROP COLUMN id;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- (name) is the primary key, this column is almost exclusively for auditing.
|
||||
-- Audit logs require a uuid as the unique identifier for a resource.
|
||||
ALTER TABLE custom_roles ADD COLUMN id uuid DEFAULT gen_random_uuid() NOT NULL;
|
||||
COMMENT ON COLUMN custom_roles.id IS 'Custom roles ID is used purely for auditing purposes. Name is a better unique identifier.';
|
||||
|
||||
-- Ensure unique uuids.
|
||||
CREATE INDEX idx_custom_roles_id ON custom_roles (id);
|
||||
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'custom_role';
|
||||
@@ -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,8 @@ 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"`
|
||||
// Custom roles ID is used purely for auditing purposes. Name is a better unique identifier.
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
// A table used to store the keys used to encrypt the database.
|
||||
|
||||
@@ -5618,7 +5618,7 @@ 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
|
||||
@@ -5667,6 +5667,7 @@ func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -5711,7 +5712,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 {
|
||||
@@ -5742,6 +5743,7 @@ func (q *sqlQuerier) UpsertCustomRole(ctx context.Context, arg UpsertCustomRoleP
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ We track the following resources:
|
||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
||||
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>true</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
|
||||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
Generated
+1
@@ -4323,6 +4323,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `organization` |
|
||||
| `oauth2_provider_app` |
|
||||
| `oauth2_provider_app_secret` |
|
||||
| `custom_role` |
|
||||
|
||||
## codersdk.Response
|
||||
|
||||
|
||||
@@ -144,6 +144,18 @@ 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:
|
||||
// String representation is much easier to visually inspect
|
||||
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())
|
||||
}
|
||||
|
||||
return leftArr, rightArr, true
|
||||
default:
|
||||
return left, right, false
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,26 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context,
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
|
||||
originalRoles, err := db.CustomRoles(ctx, database.CustomRolesParams{
|
||||
LookupRoles: []database.NameOrganizationPair{
|
||||
{
|
||||
Name: role.Name,
|
||||
OrganizationID: orgID,
|
||||
},
|
||||
},
|
||||
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[0]
|
||||
}
|
||||
|
||||
inserted, err := db.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
@@ -81,6 +116,7 @@ func (h enterpriseCustomRoleHandler) PatchOrganizationRole(ctx context.Context,
|
||||
})
|
||||
return codersdk.Role{}, false
|
||||
}
|
||||
aReq.New = inserted
|
||||
|
||||
return db2sdk.Role(inserted), true
|
||||
}
|
||||
|
||||
Generated
+2
@@ -2181,6 +2181,7 @@ export const RBACResources: RBACResource[] = [
|
||||
export type ResourceType =
|
||||
| "api_key"
|
||||
| "convert_login"
|
||||
| "custom_role"
|
||||
| "git_ssh_key"
|
||||
| "group"
|
||||
| "health_settings"
|
||||
@@ -2197,6 +2198,7 @@ export type ResourceType =
|
||||
export const ResourceTypes: ResourceType[] = [
|
||||
"api_key",
|
||||
"convert_login",
|
||||
"custom_role",
|
||||
"git_ssh_key",
|
||||
"group",
|
||||
"health_settings",
|
||||
|
||||
Reference in New Issue
Block a user