diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 90bb94e8d132a..76084b1ff54dd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9116,7 +9116,8 @@ const docTemplate = `{ "stop", "login", "logout", - "register" + "register", + "request_password_reset" ], "x-enum-varnames": [ "AuditActionCreate", @@ -9126,7 +9127,8 @@ const docTemplate = `{ "AuditActionStop", "AuditActionLogin", "AuditActionLogout", - "AuditActionRegister" + "AuditActionRegister", + "AuditActionRequestPasswordReset" ] }, "codersdk.AuditDiff": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7429cef850c0a..beff69ca22373 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8090,7 +8090,8 @@ "stop", "login", "logout", - "register" + "register", + "request_password_reset" ], "x-enum-varnames": [ "AuditActionCreate", @@ -8100,7 +8101,8 @@ "AuditActionStop", "AuditActionLogin", "AuditActionLogout", - "AuditActionRegister" + "AuditActionRegister", + "AuditActionRequestPasswordReset" ] }, "codersdk.AuditDiff": { diff --git a/coderd/audit.go b/coderd/audit.go index 6d9a23ad217a5..f764094782a2f 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -274,8 +274,15 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs func auditLogDescription(alog database.GetAuditLogsOffsetRow) string { b := strings.Builder{} + // NOTE: WriteString always returns a nil error, so we never check it - _, _ = b.WriteString("{user} ") + + // Requesting a password reset can be performed by anyone that knows the email + // of a user so saying the user performed this action might be slightly misleading. + if alog.AuditLog.Action != database.AuditActionRequestPasswordReset { + _, _ = b.WriteString("{user} ") + } + if alog.AuditLog.StatusCode >= 400 { _, _ = b.WriteString("unsuccessfully attempted to ") _, _ = b.WriteString(string(alog.AuditLog.Action)) @@ -298,8 +305,12 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow) string { return b.String() } - _, _ = b.WriteString(" ") - _, _ = b.WriteString(codersdk.ResourceType(alog.AuditLog.ResourceType).FriendlyString()) + if alog.AuditLog.Action == database.AuditActionRequestPasswordReset { + _, _ = b.WriteString(" for") + } else { + _, _ = b.WriteString(" ") + _, _ = b.WriteString(codersdk.ResourceType(alog.AuditLog.ResourceType).FriendlyString()) + } if alog.AuditLog.ResourceType == database.ResourceTypeConvertLogin { _, _ = b.WriteString(" to") diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 626d00cc81b41..382cab743fb39 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -19,7 +19,8 @@ CREATE TYPE audit_action AS ENUM ( 'stop', 'login', 'logout', - 'register' + 'register', + 'request_password_reset' ); CREATE TYPE automatic_updates AS ENUM ( diff --git a/coderd/database/migrations/000268_add_audit_action_request_password_reset.down.sql b/coderd/database/migrations/000268_add_audit_action_request_password_reset.down.sql new file mode 100644 index 0000000000000..d1d1637f4fa90 --- /dev/null +++ b/coderd/database/migrations/000268_add_audit_action_request_password_reset.down.sql @@ -0,0 +1,2 @@ +-- It's not possible to drop enum values from enum types, so the UP has "IF NOT +-- EXISTS". diff --git a/coderd/database/migrations/000268_add_audit_action_request_password_reset.up.sql b/coderd/database/migrations/000268_add_audit_action_request_password_reset.up.sql new file mode 100644 index 0000000000000..81371517202fc --- /dev/null +++ b/coderd/database/migrations/000268_add_audit_action_request_password_reset.up.sql @@ -0,0 +1,2 @@ +ALTER TYPE audit_action + ADD VALUE IF NOT EXISTS 'request_password_reset'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 05b4c404ea16f..c44aa6011bc22 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -138,14 +138,15 @@ func AllAppSharingLevelValues() []AppSharingLevel { type AuditAction string const ( - AuditActionCreate AuditAction = "create" - AuditActionWrite AuditAction = "write" - AuditActionDelete AuditAction = "delete" - AuditActionStart AuditAction = "start" - AuditActionStop AuditAction = "stop" - AuditActionLogin AuditAction = "login" - AuditActionLogout AuditAction = "logout" - AuditActionRegister AuditAction = "register" + AuditActionCreate AuditAction = "create" + AuditActionWrite AuditAction = "write" + AuditActionDelete AuditAction = "delete" + AuditActionStart AuditAction = "start" + AuditActionStop AuditAction = "stop" + AuditActionLogin AuditAction = "login" + AuditActionLogout AuditAction = "logout" + AuditActionRegister AuditAction = "register" + AuditActionRequestPasswordReset AuditAction = "request_password_reset" ) func (e *AuditAction) Scan(src interface{}) error { @@ -192,7 +193,8 @@ func (e AuditAction) Valid() bool { AuditActionStop, AuditActionLogin, AuditActionLogout, - AuditActionRegister: + AuditActionRegister, + AuditActionRequestPasswordReset: return true } return false @@ -208,6 +210,7 @@ func AllAuditActionValues() []AuditAction { AuditActionLogin, AuditActionLogout, AuditActionRegister, + AuditActionRequestPasswordReset, } } diff --git a/coderd/userauth.go b/coderd/userauth.go index 0ff3dfa8f97cc..85ab0d77e6cc1 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -220,7 +220,7 @@ func (api *API) postRequestOneTimePasscode(rw http.ResponseWriter, r *http.Reque Audit: *auditor, Log: api.Logger, Request: r, - Action: database.AuditActionWrite, + Action: database.AuditActionRequestPasswordReset, }) ) defer commitAudit() @@ -253,6 +253,7 @@ func (api *API) postRequestOneTimePasscode(rw http.ResponseWriter, r *http.Reque } // We continue if err == sql.ErrNoRows to help prevent a timing-based attack. aReq.Old = user + aReq.UserID = user.ID passcode := uuid.New() passcodeExpiresAt := dbtime.Now().Add(api.OneTimePasscodeValidityPeriod) @@ -365,6 +366,7 @@ func (api *API) postChangePasswordWithOneTimePasscode(rw http.ResponseWriter, r } // We continue if err == sql.ErrNoRows to help prevent a timing-based attack. aReq.Old = user + aReq.UserID = user.ID equal, err := userpassword.Compare(string(user.HashedOneTimePasscode), req.OneTimePasscode) if err != nil { diff --git a/codersdk/audit.go b/codersdk/audit.go index 7d83c8e238ce0..9fe51e5f24a5f 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -86,14 +86,15 @@ func (r ResourceType) FriendlyString() string { type AuditAction string const ( - AuditActionCreate AuditAction = "create" - AuditActionWrite AuditAction = "write" - AuditActionDelete AuditAction = "delete" - AuditActionStart AuditAction = "start" - AuditActionStop AuditAction = "stop" - AuditActionLogin AuditAction = "login" - AuditActionLogout AuditAction = "logout" - AuditActionRegister AuditAction = "register" + AuditActionCreate AuditAction = "create" + AuditActionWrite AuditAction = "write" + AuditActionDelete AuditAction = "delete" + AuditActionStart AuditAction = "start" + AuditActionStop AuditAction = "stop" + AuditActionLogin AuditAction = "login" + AuditActionLogout AuditAction = "logout" + AuditActionRegister AuditAction = "register" + AuditActionRequestPasswordReset AuditAction = "request_password_reset" ) func (a AuditAction) Friendly() string { @@ -114,6 +115,8 @@ func (a AuditAction) Friendly() string { return "logged out" case AuditActionRegister: return "registered" + case AuditActionRequestPasswordReset: + return "password reset requested" default: return "unknown" } diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index c4b9499f8b966..b22055ff18b5a 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -25,7 +25,7 @@ We track the following resources: | Organization
|
FieldTracked
created_atfalse
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| | 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
template_idtrue
updated_atfalse
| -| User
create, write, delete |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodetrue
hashed_passwordtrue
idtrue
last_seen_atfalse
login_typetrue
must_reset_passwordtrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| +| 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
must_reset_passwordtrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
theme_preferencefalse
updated_atfalse
usernametrue
| | Workspace
create, write, delete |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| | WorkspaceBuild
start, stop |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
|
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index ac8636f3e6f46..ed3800b3a27cd 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -513,16 +513,17 @@ #### Enumerated Values -| Value | -| ---------- | -| `create` | -| `write` | -| `delete` | -| `start` | -| `stop` | -| `login` | -| `logout` | -| `register` | +| Value | +| ------------------------ | +| `create` | +| `write` | +| `delete` | +| `start` | +| `stop` | +| `login` | +| `logout` | +| `register` | +| `request_password_reset` | ## codersdk.AuditDiff diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 15eaaeb11b4f5..baa9f33b18786 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -145,7 +145,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "theme_preference": ActionIgnore, "name": ActionTrack, "github_com_user_id": ActionIgnore, - "hashed_one_time_passcode": ActionSecret, // Do not expose a user's one time passcode. + "hashed_one_time_passcode": ActionIgnore, "one_time_passcode_expires_at": ActionTrack, "must_reset_password": ActionTrack, }, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 76be331a526cf..e55167ef03f88 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2098,8 +2098,8 @@ export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace" export const AgentSubsystems: AgentSubsystem[] = ["envbox", "envbuilder", "exectrace"] // From codersdk/audit.go -export type AuditAction = "create" | "delete" | "login" | "logout" | "register" | "start" | "stop" | "write" -export const AuditActions: AuditAction[] = ["create", "delete", "login", "logout", "register", "start", "stop", "write"] +export type AuditAction = "create" | "delete" | "login" | "logout" | "register" | "request_password_reset" | "start" | "stop" | "write" +export const AuditActions: AuditAction[] = ["create", "delete", "login", "logout", "register", "request_password_reset", "start", "stop", "write"] // From codersdk/workspaces.go export type AutomaticUpdates = "always" | "never" diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.stories.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.stories.tsx index a8c1e2435475e..dd2c88f5be50b 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.stories.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockAuditLog, + MockAuditLogRequestPasswordReset, MockAuditLogSuccessfulLogin, MockAuditLogUnsuccessfulLoginKnownUser, MockAuditLogWithWorkspaceBuild, @@ -57,6 +58,12 @@ export const UnsuccessfulLoginForUnknownUser: Story = { }, }; +export const RequestPasswordReset: Story = { + args: { + auditLog: MockAuditLogRequestPasswordReset, + }, +}; + export const CreateUser: Story = { args: { auditLog: { diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx index 33a4f24b58385..584269c515190 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx @@ -9,6 +9,14 @@ const getDiffValue = (value: unknown): string => { return `"${value}"`; } + if (isTimeObject(value)) { + if (!value.Valid) { + return "null"; + } + + return new Date(value.Time).toLocaleString(); + } + if (Array.isArray(value)) { const values = value.map((v) => getDiffValue(v)); return `[${values.join(", ")}]`; @@ -21,6 +29,19 @@ const getDiffValue = (value: unknown): string => { return String(value); }; +const isTimeObject = ( + value: unknown, +): value is { Time: string; Valid: boolean } => { + return ( + value !== null && + typeof value === "object" && + "Time" in value && + typeof value.Time === "string" && + "Valid" in value && + typeof value.Valid === "boolean" + ); +}; + interface AuditLogDiffProps { diff: AuditDiff; } diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx index f6b601486a833..12d57b63047e8 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx @@ -10,6 +10,7 @@ import { MockAuditLog, MockAuditLog2, MockAuditLogGitSSH, + MockAuditLogRequestPasswordReset, MockAuditLogWithDeletedResource, MockAuditLogWithWorkspaceBuild, MockUser, @@ -122,6 +123,12 @@ export const WithOrganization: Story = { }, }; +export const WithDateDiffValue: Story = { + args: { + auditLog: MockAuditLogRequestPasswordReset, + }, +}; + export const NoUserAgent: Story = { args: { auditLog: { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 7b654e54c48a2..0db6e80d435d6 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2600,6 +2600,32 @@ export const MockAuditLogUnsuccessfulLoginKnownUser: TypesGen.AuditLog = { status_code: 401, }; +export const MockAuditLogRequestPasswordReset: TypesGen.AuditLog = { + ...MockAuditLog, + resource_type: "user", + resource_target: "member", + action: "request_password_reset", + description: "password reset requested for {target}", + diff: { + hashed_password: { + old: "", + new: "", + secret: true, + }, + one_time_passcode_expires_at: { + old: { + Time: "0001-01-01T00:00:00Z", + Valid: false, + }, + new: { + Time: "2024-10-22T09:03:23.961702Z", + Valid: true, + }, + secret: false, + }, + }, +}; + export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { credits_consumed: 0, budget: 100, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy