Skip to content

Commit a325138

Browse files
waleedlatif1claude
andcommitted
feat(audit): add audit logging for passwords, credentials, and schedules
- Add PASSWORD_RESET_REQUESTED audit on forget-password with user lookup - Add CREDENTIAL_CREATED/UPDATED/DELETED audit on credential CRUD routes with metadata (credentialType, providerId, updatedFields, envKey) - Add SCHEDULE_CREATED audit on schedule creation with cron/timezone metadata - Fix SCHEDULE_DELETED (was incorrectly using SCHEDULE_UPDATED for deletes) - Enhance existing schedule update/disable/reactivate audit with structured metadata (operation, updatedFields, sourceType, previousStatus) - Add CREDENTIAL resource type and Credential filter option to audit logs UI - Enhance password reset completed description with user email Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bc4788a commit a325138

8 files changed

Lines changed: 150 additions & 18 deletions

File tree

apps/sim/app/api/auth/forget-password/route.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { db } from '@sim/db'
2+
import { user } from '@sim/db/schema'
13
import { createLogger } from '@sim/logger'
4+
import { eq } from 'drizzle-orm'
25
import { type NextRequest, NextResponse } from 'next/server'
36
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
48
import { auth } from '@/lib/auth'
59
import { isSameOrigin } from '@/lib/core/utils/validation'
610

@@ -51,6 +55,24 @@ export async function POST(request: NextRequest) {
5155
method: 'POST',
5256
})
5357

58+
const [existingUser] = await db
59+
.select({ id: user.id, name: user.name, email: user.email })
60+
.from(user)
61+
.where(eq(user.email, email))
62+
.limit(1)
63+
64+
if (existingUser) {
65+
recordAudit({
66+
actorId: existingUser.id,
67+
actorName: existingUser.name,
68+
actorEmail: existingUser.email,
69+
action: AuditAction.PASSWORD_RESET_REQUESTED,
70+
resourceType: AuditResourceType.PASSWORD,
71+
description: 'Password reset requested',
72+
request,
73+
})
74+
}
75+
5476
return NextResponse.json({ success: true })
5577
} catch (error) {
5678
logger.error('Error requesting password reset:', { error })

apps/sim/app/api/credentials/[id]/route.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { encryptSecret } from '@/lib/core/security/encryption'
910
import { generateId } from '@/lib/core/utils/uuid'
@@ -166,6 +167,21 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
166167
updates.updatedAt = new Date()
167168
await db.update(credential).set(updates).where(eq(credential.id, id))
168169

170+
recordAudit({
171+
workspaceId: access.credential.workspaceId,
172+
actorId: session.user.id,
173+
action: AuditAction.CREDENTIAL_UPDATED,
174+
resourceType: AuditResourceType.CREDENTIAL,
175+
resourceId: id,
176+
resourceName: access.credential.displayName,
177+
description: `Updated ${access.credential.type} credential "${access.credential.displayName}"`,
178+
metadata: {
179+
credentialType: access.credential.type,
180+
updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
181+
},
182+
request,
183+
})
184+
169185
const row = await getCredentialResponse(id, session.user.id)
170186
return NextResponse.json({ credential: row }, { status: 200 })
171187
} catch (error) {
@@ -249,6 +265,18 @@ export async function DELETE(
249265
{ groups: { workspace: access.credential.workspaceId } }
250266
)
251267

268+
recordAudit({
269+
workspaceId: access.credential.workspaceId,
270+
actorId: session.user.id,
271+
action: AuditAction.CREDENTIAL_DELETED,
272+
resourceType: AuditResourceType.CREDENTIAL,
273+
resourceId: id,
274+
resourceName: access.credential.displayName,
275+
description: `Deleted personal env credential "${access.credential.envKey}"`,
276+
metadata: { credentialType: 'env_personal', envKey: access.credential.envKey },
277+
request,
278+
})
279+
252280
return NextResponse.json({ success: true }, { status: 200 })
253281
}
254282

@@ -302,6 +330,18 @@ export async function DELETE(
302330
{ groups: { workspace: access.credential.workspaceId } }
303331
)
304332

333+
recordAudit({
334+
workspaceId: access.credential.workspaceId,
335+
actorId: session.user.id,
336+
action: AuditAction.CREDENTIAL_DELETED,
337+
resourceType: AuditResourceType.CREDENTIAL,
338+
resourceId: id,
339+
resourceName: access.credential.displayName,
340+
description: `Deleted workspace env credential "${access.credential.envKey}"`,
341+
metadata: { credentialType: 'env_workspace', envKey: access.credential.envKey },
342+
request,
343+
})
344+
305345
return NextResponse.json({ success: true }, { status: 200 })
306346
}
307347

@@ -318,6 +358,21 @@ export async function DELETE(
318358
{ groups: { workspace: access.credential.workspaceId } }
319359
)
320360

361+
recordAudit({
362+
workspaceId: access.credential.workspaceId,
363+
actorId: session.user.id,
364+
action: AuditAction.CREDENTIAL_DELETED,
365+
resourceType: AuditResourceType.CREDENTIAL,
366+
resourceId: id,
367+
resourceName: access.credential.displayName,
368+
description: `Deleted ${access.credential.type} credential "${access.credential.displayName}"`,
369+
metadata: {
370+
credentialType: access.credential.type,
371+
providerId: access.credential.providerId,
372+
},
373+
request,
374+
})
375+
321376
return NextResponse.json({ success: true }, { status: 200 })
322377
} catch (error) {
323378
logger.error('Failed to delete credential', error)

apps/sim/app/api/credentials/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { encryptSecret } from '@/lib/core/security/encryption'
910
import { generateRequestId } from '@/lib/core/utils/request'
@@ -612,6 +613,21 @@ export async function POST(request: NextRequest) {
612613
}
613614
)
614615

616+
recordAudit({
617+
workspaceId,
618+
actorId: session.user.id,
619+
action: AuditAction.CREDENTIAL_CREATED,
620+
resourceType: AuditResourceType.CREDENTIAL,
621+
resourceId: credentialId,
622+
resourceName: resolvedDisplayName,
623+
description: `Created ${type} credential "${resolvedDisplayName}"`,
624+
metadata: {
625+
credentialType: type,
626+
providerId: resolvedProviderId,
627+
},
628+
request,
629+
})
630+
615631
return NextResponse.json({ credential: created }, { status: 201 })
616632
} catch (error: any) {
617633
if (error?.code === '23505') {

apps/sim/app/api/schedules/[id]/route.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type ScheduleRow = {
3838
timezone: string | null
3939
sourceType: string | null
4040
sourceWorkspaceId: string | null
41+
jobTitle: string | null
4142
}
4243

4344
async function fetchAndAuthorize(
@@ -55,6 +56,7 @@ async function fetchAndAuthorize(
5556
timezone: workflowSchedule.timezone,
5657
sourceType: workflowSchedule.sourceType,
5758
sourceWorkspaceId: workflowSchedule.sourceWorkspaceId,
59+
jobTitle: workflowSchedule.jobTitle,
5860
})
5961
.from(workflowSchedule)
6062
.where(and(eq(workflowSchedule.id, scheduleId), isNull(workflowSchedule.archivedAt)))
@@ -147,10 +149,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
147149
action: AuditAction.SCHEDULE_UPDATED,
148150
resourceType: AuditResourceType.SCHEDULE,
149151
resourceId: scheduleId,
150-
actorName: session.user.name ?? undefined,
151-
actorEmail: session.user.email ?? undefined,
152-
description: `Disabled schedule ${scheduleId}`,
153-
metadata: {},
152+
resourceName: schedule.jobTitle ?? undefined,
153+
description: `Disabled schedule "${schedule.jobTitle ?? scheduleId}"`,
154+
metadata: {
155+
operation: 'disable',
156+
sourceType: schedule.sourceType,
157+
previousStatus: schedule.status,
158+
},
154159
request,
155160
})
156161

@@ -207,10 +212,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
207212
action: AuditAction.SCHEDULE_UPDATED,
208213
resourceType: AuditResourceType.SCHEDULE,
209214
resourceId: scheduleId,
210-
actorName: session.user.name ?? undefined,
211-
actorEmail: session.user.email ?? undefined,
212-
description: `Updated job schedule ${scheduleId}`,
213-
metadata: {},
215+
resourceName: schedule.jobTitle ?? undefined,
216+
description: `Updated job schedule "${schedule.jobTitle ?? scheduleId}"`,
217+
metadata: {
218+
operation: 'update',
219+
updatedFields: Object.keys(setFields).filter((k) => k !== 'updatedAt'),
220+
},
214221
request,
215222
})
216223

@@ -249,10 +256,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
249256
action: AuditAction.SCHEDULE_UPDATED,
250257
resourceType: AuditResourceType.SCHEDULE,
251258
resourceId: scheduleId,
252-
actorName: session.user.name ?? undefined,
253-
actorEmail: session.user.email ?? undefined,
254-
description: `Reactivated schedule ${scheduleId}`,
255-
metadata: { cronExpression: schedule.cronExpression, timezone: schedule.timezone },
259+
resourceName: schedule.jobTitle ?? undefined,
260+
description: `Reactivated schedule "${schedule.jobTitle ?? scheduleId}"`,
261+
metadata: {
262+
operation: 'reactivate',
263+
sourceType: schedule.sourceType,
264+
cronExpression: schedule.cronExpression,
265+
timezone: schedule.timezone,
266+
},
256267
request,
257268
})
258269

@@ -289,13 +300,16 @@ export async function DELETE(
289300
recordAudit({
290301
workspaceId,
291302
actorId: session.user.id,
292-
action: AuditAction.SCHEDULE_UPDATED,
303+
action: AuditAction.SCHEDULE_DELETED,
293304
resourceType: AuditResourceType.SCHEDULE,
294305
resourceId: scheduleId,
295-
actorName: session.user.name ?? undefined,
296-
actorEmail: session.user.email ?? undefined,
297-
description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} ${scheduleId}`,
298-
metadata: {},
306+
resourceName: schedule.jobTitle ?? undefined,
307+
description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} "${schedule.jobTitle ?? scheduleId}"`,
308+
metadata: {
309+
sourceType: schedule.sourceType,
310+
cronExpression: schedule.cronExpression,
311+
timezone: schedule.timezone,
312+
},
299313
request,
300314
})
301315

apps/sim/app/api/schedules/route.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { workflow, workflowDeploymentVersion, workflowSchedule } from '@sim/db/s
33
import { createLogger } from '@sim/logger'
44
import { and, eq, isNull, or } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
6+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
67
import { getSession } from '@/lib/auth'
78
import { generateRequestId } from '@/lib/core/utils/request'
89
import { generateId } from '@/lib/core/utils/uuid'
@@ -279,6 +280,23 @@ export async function POST(req: NextRequest) {
279280
lifecycle,
280281
})
281282

283+
recordAudit({
284+
workspaceId,
285+
actorId: session.user.id,
286+
action: AuditAction.SCHEDULE_CREATED,
287+
resourceType: AuditResourceType.SCHEDULE,
288+
resourceId: id,
289+
resourceName: title.trim(),
290+
description: `Created job schedule "${title.trim()}"`,
291+
metadata: {
292+
cronExpression,
293+
timezone,
294+
lifecycle,
295+
maxRuns: maxRuns ?? null,
296+
},
297+
request: req,
298+
})
299+
282300
captureServerEvent(
283301
session.user.id,
284302
'scheduled_task_created',

apps/sim/ee/audit-logs/components/audit-logs.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [
1919
{ label: 'BYOK Key', value: 'byok_key' },
2020
{ label: 'Chat', value: 'chat' },
2121
{ label: 'Connector', value: 'connector' },
22+
{ label: 'Credential', value: 'credential' },
2223
{ label: 'Credential Set', value: 'credential_set' },
2324
{ label: 'Custom Tool', value: 'custom_tool' },
2425
{ label: 'Document', value: 'document' },

apps/sim/lib/audit/log.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,13 @@ export const AuditAction = {
108108

109109
// OAuth / Credentials
110110
OAUTH_DISCONNECTED: 'oauth.disconnected',
111+
CREDENTIAL_CREATED: 'credential.created',
112+
CREDENTIAL_UPDATED: 'credential.updated',
111113
CREDENTIAL_RENAMED: 'credential.renamed',
112114
CREDENTIAL_DELETED: 'credential.deleted',
113115

114116
// Password
117+
PASSWORD_RESET_REQUESTED: 'password.reset_requested',
115118
PASSWORD_RESET: 'password.reset',
116119

117120
// Organizations
@@ -139,7 +142,9 @@ export const AuditAction = {
139142
SKILL_DELETED: 'skill.deleted',
140143

141144
// Schedules
145+
SCHEDULE_CREATED: 'schedule.created',
142146
SCHEDULE_UPDATED: 'schedule.updated',
147+
SCHEDULE_DELETED: 'schedule.deleted',
143148

144149
// Tables
145150
TABLE_CREATED: 'table.created',
@@ -186,6 +191,7 @@ export const AuditResourceType = {
186191
BYOK_KEY: 'byok_key',
187192
CHAT: 'chat',
188193
CONNECTOR: 'connector',
194+
CREDENTIAL: 'credential',
189195
CREDENTIAL_SET: 'credential_set',
190196
CUSTOM_TOOL: 'custom_tool',
191197
DOCUMENT: 'document',

apps/sim/lib/auth/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ export const auth = betterAuth({
707707
actorEmail: resetUser.email,
708708
action: AuditAction.PASSWORD_RESET,
709709
resourceType: AuditResourceType.PASSWORD,
710-
description: 'Password reset completed',
710+
description: `Password reset completed for ${resetUser.email}`,
711711
})
712712
},
713713
},

0 commit comments

Comments
 (0)