diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 791b3c6f145e8..5780443a42de1 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12937,7 +12937,12 @@ const docTemplate = `{ "organization", "oauth2_provider_app", "oauth2_provider_app_secret", - "custom_role" + "custom_role", + "organization_member", + "notification_template", + "idp_sync_settings_organization", + "idp_sync_settings_group", + "idp_sync_settings_role" ], "x-enum-varnames": [ "ResourceTypeTemplate", @@ -12956,7 +12961,12 @@ const docTemplate = `{ "ResourceTypeOrganization", "ResourceTypeOAuth2ProviderApp", "ResourceTypeOAuth2ProviderAppSecret", - "ResourceTypeCustomRole" + "ResourceTypeCustomRole", + "ResourceTypeOrganizationMember", + "ResourceTypeNotificationTemplate", + "ResourceTypeIdpSyncSettingsOrganization", + "ResourceTypeIdpSyncSettingsGroup", + "ResourceTypeIdpSyncSettingsRole" ] }, "codersdk.Response": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index abd329103579e..1ecb6d185e03c 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -11694,7 +11694,12 @@ "organization", "oauth2_provider_app", "oauth2_provider_app_secret", - "custom_role" + "custom_role", + "organization_member", + "notification_template", + "idp_sync_settings_organization", + "idp_sync_settings_group", + "idp_sync_settings_role" ], "x-enum-varnames": [ "ResourceTypeTemplate", @@ -11713,7 +11718,12 @@ "ResourceTypeOrganization", "ResourceTypeOAuth2ProviderApp", "ResourceTypeOAuth2ProviderAppSecret", - "ResourceTypeCustomRole" + "ResourceTypeCustomRole", + "ResourceTypeOrganizationMember", + "ResourceTypeNotificationTemplate", + "ResourceTypeIdpSyncSettingsOrganization", + "ResourceTypeIdpSyncSettingsGroup", + "ResourceTypeIdpSyncSettingsRole" ] }, "codersdk.Response": { diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 8d5923d575054..98e47e91893cb 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -2,6 +2,7 @@ package audit import ( "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/idpsync" ) // Auditable is mostly a marker interface. It contains a definitive list of all @@ -26,7 +27,10 @@ type Auditable interface { database.CustomRole | database.AuditableOrganizationMember | database.Organization | - database.NotificationTemplate + database.NotificationTemplate | + idpsync.OrganizationSyncSettings | + idpsync.GroupSyncSettings | + idpsync.RoleSyncSettings } // 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 c8b7bf17b4b96..05c18e32fd183 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -20,6 +20,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/tracing" ) @@ -121,11 +122,22 @@ func ResourceTarget[T Auditable](tgt T) string { return typed.Name case database.NotificationTemplate: return typed.Name + case idpsync.OrganizationSyncSettings: + return "Organization Sync" + case idpsync.GroupSyncSettings: + return "Organization Group Sync" + case idpsync.RoleSyncSettings: + return "Organization Role Sync" default: panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt)) } } +// noID can be used for resources that do not have an uuid. +// An example is singleton configuration resources. +// 51A51C = "Static" +var noID = uuid.MustParse("51A51C00-0000-0000-0000-000000000000") + func ResourceID[T Auditable](tgt T) uuid.UUID { switch typed := any(tgt).(type) { case database.Template: @@ -169,6 +181,12 @@ func ResourceID[T Auditable](tgt T) uuid.UUID { return typed.ID case database.NotificationTemplate: return typed.ID + case idpsync.OrganizationSyncSettings: + return noID // Deployment all uses the same org sync settings + case idpsync.GroupSyncSettings: + return noID // Org field on audit log has org id + case idpsync.RoleSyncSettings: + return noID // Org field on audit log has org id default: panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt)) } @@ -214,6 +232,12 @@ func ResourceType[T Auditable](tgt T) database.ResourceType { return database.ResourceTypeOrganization case database.NotificationTemplate: return database.ResourceTypeNotificationTemplate + case idpsync.OrganizationSyncSettings: + return database.ResourceTypeIdpSyncSettingsOrganization + case idpsync.RoleSyncSettings: + return database.ResourceTypeIdpSyncSettingsRole + case idpsync.GroupSyncSettings: + return database.ResourceTypeIdpSyncSettingsGroup default: panic(fmt.Sprintf("unknown resource %T for ResourceType", typed)) } @@ -261,6 +285,12 @@ func ResourceRequiresOrgID[T Auditable]() bool { return true case database.NotificationTemplate: return false + case idpsync.OrganizationSyncSettings: + return false + case idpsync.GroupSyncSettings: + return true + case idpsync.RoleSyncSettings: + return true default: panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt)) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 782bc4969d799..f91a5371f06f6 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -190,7 +190,10 @@ CREATE TYPE resource_type AS ENUM ( 'custom_role', 'organization_member', 'notifications_settings', - 'notification_template' + 'notification_template', + 'idp_sync_settings_organization', + 'idp_sync_settings_group', + 'idp_sync_settings_role' ); CREATE TYPE startup_script_behavior AS ENUM ( diff --git a/coderd/database/migrations/000281_idpsync_settings.down.sql b/coderd/database/migrations/000281_idpsync_settings.down.sql new file mode 100644 index 0000000000000..362f597df0911 --- /dev/null +++ b/coderd/database/migrations/000281_idpsync_settings.down.sql @@ -0,0 +1 @@ +-- Nothing to do diff --git a/coderd/database/migrations/000281_idpsync_settings.up.sql b/coderd/database/migrations/000281_idpsync_settings.up.sql new file mode 100644 index 0000000000000..4b5983ee71576 --- /dev/null +++ b/coderd/database/migrations/000281_idpsync_settings.up.sql @@ -0,0 +1,4 @@ +-- Allow modifications to notification templates to be audited. +ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'idp_sync_settings_organization'; +ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'idp_sync_settings_group'; +ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'idp_sync_settings_role'; diff --git a/coderd/database/models.go b/coderd/database/models.go index e5ddebcbc8b9a..e9a5f93051ba5 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1524,25 +1524,28 @@ func AllProvisionerTypeValues() []ProvisionerType { type ResourceType string const ( - ResourceTypeOrganization ResourceType = "organization" - ResourceTypeTemplate ResourceType = "template" - ResourceTypeTemplateVersion ResourceType = "template_version" - ResourceTypeUser ResourceType = "user" - ResourceTypeWorkspace ResourceType = "workspace" - ResourceTypeGitSshKey ResourceType = "git_ssh_key" - ResourceTypeApiKey ResourceType = "api_key" - ResourceTypeGroup ResourceType = "group" - ResourceTypeWorkspaceBuild ResourceType = "workspace_build" - ResourceTypeLicense ResourceType = "license" - ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy" - ResourceTypeConvertLogin ResourceType = "convert_login" - ResourceTypeHealthSettings ResourceType = "health_settings" - ResourceTypeOauth2ProviderApp ResourceType = "oauth2_provider_app" - ResourceTypeOauth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" - ResourceTypeCustomRole ResourceType = "custom_role" - ResourceTypeOrganizationMember ResourceType = "organization_member" - ResourceTypeNotificationsSettings ResourceType = "notifications_settings" - ResourceTypeNotificationTemplate ResourceType = "notification_template" + ResourceTypeOrganization ResourceType = "organization" + ResourceTypeTemplate ResourceType = "template" + ResourceTypeTemplateVersion ResourceType = "template_version" + ResourceTypeUser ResourceType = "user" + ResourceTypeWorkspace ResourceType = "workspace" + ResourceTypeGitSshKey ResourceType = "git_ssh_key" + ResourceTypeApiKey ResourceType = "api_key" + ResourceTypeGroup ResourceType = "group" + ResourceTypeWorkspaceBuild ResourceType = "workspace_build" + ResourceTypeLicense ResourceType = "license" + ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy" + ResourceTypeConvertLogin ResourceType = "convert_login" + ResourceTypeHealthSettings ResourceType = "health_settings" + ResourceTypeOauth2ProviderApp ResourceType = "oauth2_provider_app" + ResourceTypeOauth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" + ResourceTypeCustomRole ResourceType = "custom_role" + ResourceTypeOrganizationMember ResourceType = "organization_member" + ResourceTypeNotificationsSettings ResourceType = "notifications_settings" + ResourceTypeNotificationTemplate ResourceType = "notification_template" + ResourceTypeIdpSyncSettingsOrganization ResourceType = "idp_sync_settings_organization" + ResourceTypeIdpSyncSettingsGroup ResourceType = "idp_sync_settings_group" + ResourceTypeIdpSyncSettingsRole ResourceType = "idp_sync_settings_role" ) func (e *ResourceType) Scan(src interface{}) error { @@ -1600,7 +1603,10 @@ func (e ResourceType) Valid() bool { ResourceTypeCustomRole, ResourceTypeOrganizationMember, ResourceTypeNotificationsSettings, - ResourceTypeNotificationTemplate: + ResourceTypeNotificationTemplate, + ResourceTypeIdpSyncSettingsOrganization, + ResourceTypeIdpSyncSettingsGroup, + ResourceTypeIdpSyncSettingsRole: return true } return false @@ -1627,6 +1633,9 @@ func AllResourceTypeValues() []ResourceType { ResourceTypeOrganizationMember, ResourceTypeNotificationsSettings, ResourceTypeNotificationTemplate, + ResourceTypeIdpSyncSettingsOrganization, + ResourceTypeIdpSyncSettingsGroup, + ResourceTypeIdpSyncSettingsRole, } } diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index 66d8ab08495cc..12d79bc047776 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -149,13 +149,13 @@ type OrganizationSyncSettings struct { // Field selects the claim field to be used as the created user's // organizations. If the field is the empty string, then no organization updates // will ever come from the OIDC provider. - Field string + Field string `json:"field"` // Mapping controls how organizations returned by the OIDC provider get mapped - Mapping map[string][]uuid.UUID + Mapping map[string][]uuid.UUID `json:"mapping"` // AssignDefault will ensure all users that authenticate will be // placed into the default organization. This is mostly a hack to support // legacy deployments. - AssignDefault bool + AssignDefault bool `json:"assign_default"` } func (s *OrganizationSyncSettings) Set(v string) error { diff --git a/codersdk/audit.go b/codersdk/audit.go index 9fe51e5f24a5f..307eeb275b61c 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -30,10 +30,13 @@ const ( ResourceTypeOrganization ResourceType = "organization" ResourceTypeOAuth2ProviderApp ResourceType = "oauth2_provider_app" // nolint:gosec // This is not a secret. - ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" - ResourceTypeCustomRole ResourceType = "custom_role" - ResourceTypeOrganizationMember = "organization_member" - ResourceTypeNotificationTemplate = "notification_template" + ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret" + ResourceTypeCustomRole ResourceType = "custom_role" + ResourceTypeOrganizationMember ResourceType = "organization_member" + ResourceTypeNotificationTemplate ResourceType = "notification_template" + ResourceTypeIdpSyncSettingsOrganization ResourceType = "idp_sync_settings_organization" + ResourceTypeIdpSyncSettingsGroup ResourceType = "idp_sync_settings_group" + ResourceTypeIdpSyncSettingsRole ResourceType = "idp_sync_settings_role" ) func (r ResourceType) FriendlyString() string { @@ -78,6 +81,12 @@ func (r ResourceType) FriendlyString() string { return "organization member" case ResourceTypeNotificationTemplate: return "notification template" + case ResourceTypeIdpSyncSettingsOrganization: + return "settings" + case ResourceTypeIdpSyncSettingsGroup: + return "settings" + case ResourceTypeIdpSyncSettingsRole: + return "settings" default: return "unknown" } diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 8f39e130a7dad..092cb5fba6456 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -16,6 +16,7 @@ We track the following resources: | AuditableOrganizationMember
|
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| | CustomRole
|
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| GroupSyncSettings
|
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| | HealthSettings
|
FieldTracked
dismissed_healthcheckstrue
idfalse
| | License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| | NotificationTemplate
|
FieldTracked
actionstrue
body_templatetrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| @@ -23,6 +24,8 @@ We track the following resources: | OAuth2ProviderApp
|
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| | OAuth2ProviderAppSecret
|
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| | Organization
|
FieldTracked
created_atfalse
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| +| OrganizationSyncSettings
|
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| +| RoleSyncSettings
|
FieldTracked
fieldtrue
mappingtrue
| | Template
write, delete |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| | TemplateVersion
create, write |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| | User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index b124e7be93b26..6b91a64d02789 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4777,25 +4777,30 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o #### Enumerated Values -| Value | -| ---------------------------- | -| `template` | -| `template_version` | -| `user` | -| `workspace` | -| `workspace_build` | -| `git_ssh_key` | -| `api_key` | -| `group` | -| `license` | -| `convert_login` | -| `health_settings` | -| `notifications_settings` | -| `workspace_proxy` | -| `organization` | -| `oauth2_provider_app` | -| `oauth2_provider_app_secret` | -| `custom_role` | +| Value | +| -------------------------------- | +| `template` | +| `template_version` | +| `user` | +| `workspace` | +| `workspace_build` | +| `git_ssh_key` | +| `api_key` | +| `group` | +| `license` | +| `convert_login` | +| `health_settings` | +| `notifications_settings` | +| `workspace_proxy` | +| `organization` | +| `oauth2_provider_app` | +| `oauth2_provider_app_secret` | +| `custom_role` | +| `organization_member` | +| `notification_template` | +| `idp_sync_settings_organization` | +| `idp_sync_settings_group` | +| `idp_sync_settings_role` | ## codersdk.Response diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go index 07cd8a5fdcb87..8196238ecc841 100644 --- a/enterprise/audit/diff.go +++ b/enterprise/audit/diff.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "reflect" + "strings" "github.com/google/uuid" "golang.org/x/xerrors" @@ -49,6 +50,7 @@ func diffValues(left, right any, table Table) audit.Map { ) diffName := field.FieldType.Tag.Get("json") + diffName = strings.TrimSuffix(diffName, ",omitempty") atype, ok := diffKey[diffName] if !ok { diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 4f27d8fe06b64..4bbeefdf01e09 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -5,8 +5,10 @@ import ( "os" "reflect" "runtime" + "strings" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/codersdk" ) @@ -286,6 +288,23 @@ var auditableResourcesTypes = map[any]map[string]Action{ "method": ActionTrack, "kind": ActionTrack, }, + &idpsync.OrganizationSyncSettings{}: { + "field": ActionTrack, + "mapping": ActionTrack, + "assign_default": ActionTrack, + }, + &idpsync.GroupSyncSettings{}: { + "field": ActionTrack, + "mapping": ActionTrack, + "regex_filter": ActionTrack, + "auto_create_missing_groups": ActionTrack, + // Configured in env vars + "legacy_group_name_mapping": ActionIgnore, + }, + &idpsync.RoleSyncSettings{}: { + "field": ActionTrack, + "mapping": ActionTrack, + }, } // auditMap converts a map of struct pointers to a map of struct names as @@ -335,6 +354,7 @@ func entry(v any, f map[string]Action) (string, map[string]Action) { // This field is explicitly ignored. continue } + jsonTag = strings.TrimSuffix(jsonTag, ",omitempty") if _, ok := fcpy[jsonTag]; !ok { _, _ = fmt.Fprintf(os.Stderr, "ERROR: Audit table entry missing action for field %q in type %q\nPlease update the auditable resource types in: %s\n", d.FieldType.Name, name, self()) //nolint:revive diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index 087266462df26..e7346f8406844 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -6,6 +6,8 @@ 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/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" @@ -56,6 +58,16 @@ func (api *API) groupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + + aReq, commitAudit := audit.InitRequest[idpsync.GroupSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { httpapi.Forbidden(rw) @@ -83,7 +95,14 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques //nolint:gocritic // Requires system context to update runtime config sysCtx := dbauthz.AsSystemRestricted(ctx) - err := api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ + existing, err := api.IDPSync.GroupSyncSettings(sysCtx, org.ID, api.Database) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + aReq.Old = *existing + + err = api.IDPSync.UpdateGroupSettings(sysCtx, org.ID, api.Database, idpsync.GroupSyncSettings{ Field: req.Field, Mapping: req.Mapping, RegexFilter: req.RegexFilter, @@ -101,6 +120,7 @@ func (api *API) patchGroupIDPSyncSettings(rw http.ResponseWriter, r *http.Reques return } + aReq.New = *settings httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupSyncSettings{ Field: settings.Field, Mapping: settings.Mapping, @@ -151,6 +171,16 @@ func (api *API) roleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() org := httpmw.OrganizationParam(r) + auditor := *api.AGPL.Auditor.Load() + + aReq, commitAudit := audit.InitRequest[idpsync.RoleSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + OrganizationID: org.ID, + }) + defer commitAudit() if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings.InOrg(org.ID)) { httpapi.Forbidden(rw) @@ -164,7 +194,14 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request //nolint:gocritic // Requires system context to update runtime config sysCtx := dbauthz.AsSystemRestricted(ctx) - err := api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ + existing, err := api.IDPSync.RoleSyncSettings(sysCtx, org.ID, api.Database) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + aReq.Old = *existing + + err = api.IDPSync.UpdateRoleSettings(sysCtx, org.ID, api.Database, idpsync.RoleSyncSettings{ Field: req.Field, Mapping: req.Mapping, }) @@ -179,6 +216,7 @@ func (api *API) patchRoleIDPSyncSettings(rw http.ResponseWriter, r *http.Request return } + aReq.New = *settings httpapi.Write(ctx, rw, http.StatusOK, codersdk.RoleSyncSettings{ Field: settings.Field, Mapping: settings.Mapping, @@ -226,6 +264,14 @@ func (api *API) organizationIDPSyncSettings(rw http.ResponseWriter, r *http.Requ // @Router /settings/idpsync/organization [patch] func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + auditor := *api.AGPL.Auditor.Load() + aReq, commitAudit := audit.InitRequest[idpsync.OrganizationSyncSettings](rw, &audit.RequestParams{ + Audit: auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, + }) + defer commitAudit() if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceIdpsyncSettings) { httpapi.Forbidden(rw) @@ -239,7 +285,14 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http //nolint:gocritic // Requires system context to update runtime config sysCtx := dbauthz.AsSystemRestricted(ctx) - err := api.IDPSync.UpdateOrganizationSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ + existing, err := api.IDPSync.OrganizationSyncSettings(sysCtx, api.Database) + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + aReq.Old = *existing + + err = api.IDPSync.UpdateOrganizationSettings(sysCtx, api.Database, idpsync.OrganizationSyncSettings{ Field: req.Field, // We do not check if the mappings point to actual organizations. Mapping: req.Mapping, @@ -256,6 +309,7 @@ func (api *API) patchOrganizationIDPSyncSettings(rw http.ResponseWriter, r *http return } + aReq.New = *settings httpapi.Write(ctx, rw, http.StatusOK, codersdk.OrganizationSyncSettings{ Field: settings.Field, Mapping: settings.Mapping, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5d840d763af78..c605268c9d920 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1548,15 +1548,9 @@ export interface ResolveAutostartResponse { } // From codersdk/audit.go -export type ResourceType = "api_key" | "convert_login" | "custom_role" | "git_ssh_key" | "group" | "health_settings" | "license" | "notifications_settings" | "oauth2_provider_app" | "oauth2_provider_app_secret" | "organization" | "template" | "template_version" | "user" | "workspace" | "workspace_build" | "workspace_proxy"; +export type ResourceType = "api_key" | "convert_login" | "custom_role" | "git_ssh_key" | "group" | "health_settings" | "idp_sync_settings_group" | "idp_sync_settings_organization" | "idp_sync_settings_role" | "license" | "notification_template" | "notifications_settings" | "oauth2_provider_app" | "oauth2_provider_app_secret" | "organization" | "organization_member" | "template" | "template_version" | "user" | "workspace" | "workspace_build" | "workspace_proxy"; -// From codersdk/audit.go -export const ResourceTypeNotificationTemplate = "notification_template"; - -// From codersdk/audit.go -export const ResourceTypeOrganizationMember = "organization_member"; - -export const ResourceTypes: ResourceType[] = ["api_key", "convert_login", "custom_role", "git_ssh_key", "group", "health_settings", "license", "notifications_settings", "oauth2_provider_app", "oauth2_provider_app_secret", "organization", "template", "template_version", "user", "workspace", "workspace_build", "workspace_proxy"]; +export const ResourceTypes: ResourceType[] = ["api_key", "convert_login", "custom_role", "git_ssh_key", "group", "health_settings", "idp_sync_settings_group", "idp_sync_settings_organization", "idp_sync_settings_role", "license", "notification_template", "notifications_settings", "oauth2_provider_app", "oauth2_provider_app_secret", "organization", "organization_member", "template", "template_version", "user", "workspace", "workspace_build", "workspace_proxy"]; // From codersdk/client.go export interface Response { diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts index 7c0696a9afcbb..f07a69985d0d8 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts @@ -23,3 +23,29 @@ export const determineGroupDiff = (auditLogDiff: AuditDiff): AuditDiff => { }, }; }; + +/** + * + * @param auditLogDiff + * @returns a diff with the 'mappings' as a JSON string. Otherwise, it is [Object object] + */ +export const determineIdPSyncMappingDiff = ( + auditLogDiff: AuditDiff, +): AuditDiff => { + const old = auditLogDiff.mapping?.old as Record | undefined; + const new_ = auditLogDiff.mapping?.new as + | Record + | undefined; + if (!old || !new_) { + return auditLogDiff; + } + + return { + ...auditLogDiff, + mapping: { + old: JSON.stringify(old), + new: JSON.stringify(new_), + secret: auditLogDiff.mapping?.secret, + }, + }; +}; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index db8e7e4537cc4..909fb7cf5646e 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -16,7 +16,10 @@ import type { ThemeRole } from "theme/roles"; import userAgentParser from "ua-parser-js"; import { AuditLogDescription } from "./AuditLogDescription/AuditLogDescription"; import { AuditLogDiff } from "./AuditLogDiff/AuditLogDiff"; -import { determineGroupDiff } from "./AuditLogDiff/auditUtils"; +import { + determineGroupDiff, + determineIdPSyncMappingDiff, +} from "./AuditLogDiff/auditUtils"; const httpStatusColor = (httpStatus: number): ThemeRole => { // Treat server errors (500) as errors @@ -59,6 +62,14 @@ export const AuditLogRow: FC = ({ auditDiff = determineGroupDiff(auditLog.diff); } + if ( + auditLog.resource_type === "idp_sync_settings_organization" || + auditLog.resource_type === "idp_sync_settings_group" || + auditLog.resource_type === "idp_sync_settings_role" + ) { + auditDiff = determineIdPSyncMappingDiff(auditLog.diff); + } + const toggle = () => { if (shouldDisplayDiff) { setIsDiffOpen((v) => !v);