chore: implement audit log for custom role edits (#13494)

* chore: implement audit log for custom role edits
This commit is contained in:
Steven Masley
2024-06-07 14:11:57 -05:00
committed by GitHub
parent 056a697eff
commit 0d65143301
21 changed files with 122 additions and 16 deletions

6
coderd/apidoc/docs.go generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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

View File

@ -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))
}

View File

@ -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)

View File

@ -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{
{

View File

@ -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,

View File

@ -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);

View File

@ -0,0 +1,2 @@
DROP INDEX idx_custom_roles_id;
ALTER TABLE custom_roles DROP COLUMN id;

View File

@ -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';

View File

@ -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.

View File

@ -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
}

View File

@ -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
}