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
|
Field | Tracked |
---|
created_at | false |
description | true |
display_name | true |
icon | true |
id | false |
is_default | true |
name | true |
updated_at | true |
|
| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
activity_bump | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
autostart_block_days_of_week | true |
autostop_requirement_days_of_week | true |
autostop_requirement_weeks | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
default_ttl | true |
deleted | false |
deprecated | true |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
max_port_sharing_level | true |
name | true |
organization_display_name | false |
organization_icon | false |
organization_id | false |
organization_name | false |
provisioner | true |
require_active_version | true |
time_til_dormant | true |
time_til_dormant_autodelete | true |
updated_at | false |
user_acl | true |
|
| TemplateVersion
create, write | Field | Tracked |
---|
archived | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
external_auth_providers | false |
id | true |
job_id | false |
message | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
-| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
github_com_user_id | false |
hashed_one_time_passcode | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | true |
must_reset_password | true |
name | true |
one_time_passcode_expires_at | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
theme_preference | false |
updated_at | false |
username | true |
|
+| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
github_com_user_id | false |
hashed_one_time_passcode | false |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | true |
must_reset_password | true |
name | true |
one_time_passcode_expires_at | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
theme_preference | false |
updated_at | false |
username | true |
|
| Workspace
create, write, delete | Field | Tracked |
---|
automatic_updates | true |
autostart_schedule | true |
created_at | false |
deleted | false |
deleting_at | true |
dormant_at | true |
favorite | true |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_by_avatar_url | false |
initiator_by_username | false |
initiator_id | false |
job_id | false |
max_deadline | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
| WorkspaceProxy
| Field | Tracked |
---|
created_at | true |
deleted | false |
derp_enabled | true |
derp_only | true |
display_name | true |
icon | true |
id | true |
name | true |
region_id | true |
token_hashed_secret | true |
updated_at | false |
url | true |
version | true |
wildcard_hostname | true |
|
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