fix: show audit logs for forgot password flow (#15181)

Fixes https://github.com/coder/coder/issues/15150

Audit logs for requesting a password reset, and a user updating their
password, now show up in the audit log.
This commit is contained in:
Danielle Maywood
2024-10-22 13:47:30 +01:00
committed by GitHub
parent 297089e944
commit 5076161078
17 changed files with 130 additions and 40 deletions
+4 -2
View File
@@ -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": {
+4 -2
View File
@@ -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": {
+14 -3
View File
@@ -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")
+2 -1
View File
@@ -19,7 +19,8 @@ CREATE TYPE audit_action AS ENUM (
'stop',
'login',
'logout',
'register'
'register',
'request_password_reset'
);
CREATE TYPE automatic_updates AS ENUM (
@@ -0,0 +1,2 @@
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
-- EXISTS".
@@ -0,0 +1,2 @@
ALTER TYPE audit_action
ADD VALUE IF NOT EXISTS 'request_password_reset';
+12 -9
View File
@@ -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,
}
}
+3 -1
View File
@@ -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 {
+11 -8
View File
@@ -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"
}
+1 -1
View File
@@ -25,7 +25,7 @@ We track the following resources:
| Organization<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>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>must_reset_password</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>must_reset_password</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<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>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
+11 -10
View File
@@ -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
+1 -1
View File
@@ -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,
},
+2 -2
View File
@@ -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"
@@ -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: {
@@ -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;
}
@@ -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: {
+26
View File
@@ -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,