mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
Generated
+4
-2
@@ -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": {
|
||||
|
||||
Generated
+4
-2
@@ -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
@@ -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")
|
||||
|
||||
Generated
+2
-1
@@ -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';
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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> |
|
||||
|
||||
Generated
+11
-10
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
+7
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user