From 05ce32c032eb9a8aee81ab4746b647e58e173d4d Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@gmail.com> Date: Thu, 6 Jun 2024 11:44:10 -0500 Subject: [PATCH 1/6] chore: implement audit log for custom role edits --- coderd/audit/diff.go | 3 +- coderd/audit/request.go | 8 ++++ coderd/coderdtest/coderdtest.go | 2 + coderd/database/dbauthz/customroles_test.go | 1 - coderd/database/dbmem/dbmem.go | 1 + coderd/database/dump.sql | 8 +++- .../000217_org_custom_role_audit.down.sql | 2 + .../000217_org_custom_role_audit.up.sql | 9 +++++ coderd/database/models.go | 6 ++- coderd/database/queries.sql.go | 6 ++- coderd/roles.go | 6 +-- codersdk/audit.go | 3 ++ enterprise/audit/diff.go | 13 +++++++ enterprise/audit/table.go | 12 ++++++ enterprise/coderd/coderd.go | 2 +- enterprise/coderd/roles.go | 38 ++++++++++++++++++- site/src/api/typesGenerated.ts | 2 + 17 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 coderd/database/migrations/000217_org_custom_role_audit.down.sql create mode 100644 coderd/database/migrations/000217_org_custom_role_audit.up.sql 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 b98af8fd23889..006f35d2a2fe9 100644 --- a/coderd/database/dbauthz/customroles_test.go +++ b/coderd/database/dbauthz/customroles_test.go @@ -244,7 +244,6 @@ func TestUpsertCustomRoles(t *testing.T) { } else { require.NoError(t, err) - // Verify we can fetch the role roles, err := az.CustomRoles(ctx, database.CustomRolesParams{ LookupRoles: []database.NameOrganizationPair{ { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 3f9ef73048e6b..147eb8eca6a05 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -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, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index c6faa00c65fc5..03bb380706624 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -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,7 +418,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'; @@ -1642,6 +1644,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/000217_org_custom_role_audit.down.sql b/coderd/database/migrations/000217_org_custom_role_audit.down.sql new file mode 100644 index 0000000000000..5ad6106f2fc26 --- /dev/null +++ b/coderd/database/migrations/000217_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/000217_org_custom_role_audit.up.sql b/coderd/database/migrations/000217_org_custom_role_audit.up.sql new file mode 100644 index 0000000000000..7a58bb3d7fa14 --- /dev/null +++ b/coderd/database/migrations/000217_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 aa69054abc2aa..affd3556f4140 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -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/queries.sql.go b/coderd/database/queries.sql.go index 5dff8c05e05a7..823cf2cc45796 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -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 } 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..3a162a1b5ea80 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,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 } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fae41504f1c34..a53717e3e0229 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -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", From 91cc834f9eb1d8122c4f1a2ea4a8b40cb99912d0 Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@gmail.com> Date: Thu, 6 Jun 2024 12:09:46 -0500 Subject: [PATCH 2/6] make fmt --- enterprise/audit/diff.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go index 5a24c93c4991c..007f475f6f5eb 100644 --- a/enterprise/audit/diff.go +++ b/enterprise/audit/diff.go @@ -145,6 +145,7 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) { 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 { @@ -154,8 +155,6 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) { 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 From be6deceb32ff354788b49c5a854d7e1d26252899 Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@gmail.com> Date: Thu, 6 Jun 2024 12:31:56 -0500 Subject: [PATCH 3/6] make gen --- coderd/apidoc/docs.go | 6 ++++-- coderd/apidoc/swagger.json | 6 ++++-- coderd/database/dump.sql | 2 ++ .../database/migrations/000217_org_custom_role_audit.up.sql | 1 + coderd/database/models.go | 3 ++- docs/admin/audit-logs.md | 1 + docs/api/schemas.md | 1 + 7 files changed, 15 insertions(+), 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8905c8a09cba5..5fe1e929d83bd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 3ab826d3920da..18eb052c3fd64 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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": { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 03bb380706624..83eea6e3583a6 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -426,6 +426,8 @@ COMMENT ON TABLE custom_roles IS 'Custom roles allow dynamic roles expanded at r 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, diff --git a/coderd/database/migrations/000217_org_custom_role_audit.up.sql b/coderd/database/migrations/000217_org_custom_role_audit.up.sql index 7a58bb3d7fa14..784aea22c20f7 100644 --- a/coderd/database/migrations/000217_org_custom_role_audit.up.sql +++ b/coderd/database/migrations/000217_org_custom_role_audit.up.sql @@ -3,6 +3,7 @@ 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; +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); diff --git a/coderd/database/models.go b/coderd/database/models.go index affd3556f4140..8a558f5beeb0b 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1795,7 +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"` - ID uuid.UUID `db:"id" json:"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. diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index fada57f32065f..34c4e8c9a8dc3 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -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> | diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 24af8aece05e6..55070fb629864 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -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 From 1c9fd5273348a8b7b8dfcbcbf2f9ee3fa401762c Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@gmail.com> Date: Fri, 7 Jun 2024 10:12:11 -0500 Subject: [PATCH 4/6] update migration file --- ..._role_audit.down.sql => 000218_org_custom_role_audit.down.sql} | 0 ...stom_role_audit.up.sql => 000218_org_custom_role_audit.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000217_org_custom_role_audit.down.sql => 000218_org_custom_role_audit.down.sql} (100%) rename coderd/database/migrations/{000217_org_custom_role_audit.up.sql => 000218_org_custom_role_audit.up.sql} (100%) diff --git a/coderd/database/migrations/000217_org_custom_role_audit.down.sql b/coderd/database/migrations/000218_org_custom_role_audit.down.sql similarity index 100% rename from coderd/database/migrations/000217_org_custom_role_audit.down.sql rename to coderd/database/migrations/000218_org_custom_role_audit.down.sql diff --git a/coderd/database/migrations/000217_org_custom_role_audit.up.sql b/coderd/database/migrations/000218_org_custom_role_audit.up.sql similarity index 100% rename from coderd/database/migrations/000217_org_custom_role_audit.up.sql rename to coderd/database/migrations/000218_org_custom_role_audit.up.sql From fa6c36ef7cdea20a02d906daecc012f5a3083f27 Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@gmail.com> Date: Fri, 7 Jun 2024 11:17:23 -0500 Subject: [PATCH 5/6] add comment --- coderd/database/dbauthz/customroles_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/database/dbauthz/customroles_test.go b/coderd/database/dbauthz/customroles_test.go index 006f35d2a2fe9..814ba88a1b18c 100644 --- a/coderd/database/dbauthz/customroles_test.go +++ b/coderd/database/dbauthz/customroles_test.go @@ -244,6 +244,7 @@ func TestUpsertCustomRoles(t *testing.T) { } else { require.NoError(t, err) + // Verify the role is fetched with the lookup filter. roles, err := az.CustomRoles(ctx, database.CustomRolesParams{ LookupRoles: []database.NameOrganizationPair{ { From 4a8d5c44d82699d723598a4fe463653130ff7a76 Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@gmail.com> Date: Fri, 7 Jun 2024 11:19:06 -0500 Subject: [PATCH 6/6] remove excess sql --- .../database/migrations/000218_org_custom_role_audit.up.sql | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/coderd/database/migrations/000218_org_custom_role_audit.up.sql b/coderd/database/migrations/000218_org_custom_role_audit.up.sql index 784aea22c20f7..a780f34960907 100644 --- a/coderd/database/migrations/000218_org_custom_role_audit.up.sql +++ b/coderd/database/migrations/000218_org_custom_role_audit.up.sql @@ -1,7 +1,5 @@ --- 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. +-- 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.';