Skip to content

Commit 093c50e

Browse files
authored
fix(audit): cap rollup writes process-wide and name the run on credential access entries (#9407)
* fix(audit): cap rollup writes process-wide and name the run on credential access entries * fix(audit): record Vertex service-account credential access
1 parent 05e49a8 commit 093c50e

9 files changed

Lines changed: 79 additions & 8 deletions

File tree

‎apps/docs/content/docs/platform/enterprise/audit-logs.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ Events use identifiers such as `resource.action`. These categories are represent
105105

106106
Authentication failures and password-reset requests use an anonymous actor and record the target account separately. Impersonation and administrative session revocations identify the operator and target organization. Table-row mutations record summaries and counts without copying row contents.
107107

108-
When a workflow run makes the same kind of change to a table many times in quick succession, such as inserting rows in a loop, the repeats are merged into one `table.updated` entry. The merged entry keeps every row ID in `metadata.rowIds`, sums the counts, and records in `metadata.rollup` how many changes it covers and when the first and last happened. The actor of any workflow-made entry names the workflow (`metadata.actor.workflowId`) and the run (`metadata.actor.executionId`). Credential use by a workflow run is merged the same way: one `credential.accessed` entry per credential, counting the repeats.
108+
When a workflow run makes the same kind of change to a table many times in quick succession, such as inserting rows in a loop, the repeats are merged into one `table.updated` entry. The merged entry keeps every row ID in `metadata.rowIds`, sums the counts, and records in `metadata.rollup` how many changes it covers and when the first and last happened. The actor of any workflow-made entry names the workflow (`metadata.actor.workflowId`) and the run (`metadata.actor.executionId`). Credential use by a workflow run is merged the same way: one `credential.accessed` entry per credential, counting the repeats, that names the workflow (`metadata.workflowId`) and the run (`metadata.executionId`).
109109

110110
Most audit writes are best effort: an outage or process loss can prevent persistence after an action succeeds. Deployment activation uses an awaited, idempotent insert before its outbox checkpoint. The feed is not a complete tool-call stream or a guaranteed transactional ledger for every operation.
111111

‎apps/sim/executor/handlers/agent/handler.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2988,6 +2988,7 @@ export class AgentBlockHandler implements BlockHandler {
29882988
actingUserId: ctx.userId,
29892989
workspaceId: ctx.workspaceId,
29902990
workflowId: ctx.workflowId,
2991+
executionId: ctx.executionId,
29912992
callerLabel: 'vertex-agent',
29922993
})
29932994
}

‎apps/sim/executor/handlers/evaluator/handler.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ export class EvaluatorBlockHandler implements BlockHandler {
185185
actingUserId: ctx.userId,
186186
workspaceId: ctx.workspaceId,
187187
workflowId: ctx.workflowId,
188+
executionId: ctx.executionId,
188189
callerLabel: 'vertex-evaluator',
189190
})
190191
}

‎apps/sim/executor/handlers/router/handler.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export class RouterBlockHandler implements BlockHandler {
120120
actingUserId: ctx.userId,
121121
workspaceId: ctx.workspaceId,
122122
workflowId: ctx.workflowId,
123+
executionId: ctx.executionId,
123124
callerLabel: 'vertex-router',
124125
})
125126
}
@@ -289,6 +290,7 @@ export class RouterBlockHandler implements BlockHandler {
289290
actingUserId: ctx.userId,
290291
workspaceId: ctx.workspaceId,
291292
workflowId: ctx.workflowId,
293+
executionId: ctx.executionId,
292294
callerLabel: 'vertex-router',
293295
})
294296
}

‎apps/sim/executor/utils/credential-token.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ResolveExecutorCredentialTokenParams {
2222
credentialId: string
2323
userId?: string
2424
workflowId?: string
25+
executionId?: string
2526
/** Tool consuming the token; required by the managed-OAuth scope policy. */
2627
toolId?: string
2728
/** Display label for the thrown failure ("Failed to obtain credential for X: ..."). */
@@ -112,6 +113,7 @@ export async function resolveExecutorCredentialToken(
112113
requestId,
113114
credentialId,
114115
workflowId,
116+
executionId: params.executionId,
115117
toolId,
116118
scopes: params.scopes,
117119
impersonateEmail: params.impersonateEmail,

‎apps/sim/executor/utils/vertex-credential.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { canUseCredential, getCredentialActorContext } from '@/lib/credentials/access'
33
import { getServiceAccountToken } from '@/lib/oauth/credential-service'
4+
import { recordCredentialAccess } from '@/lib/oauth/token-resolution'
45
import { resolveExecutorCredentialToken } from '@/executor/utils/credential-token'
56

67
const logger = createLogger('VertexCredential')
@@ -12,6 +13,7 @@ export interface ResolveVertexCredentialParams {
1213
workspaceId: string | null | undefined
1314
/** Pins the token request to this workflow's workspace. */
1415
workflowId?: string
16+
executionId?: string
1517
callerLabel?: string
1618
}
1719

@@ -27,6 +29,7 @@ export async function resolveVertexCredential({
2729
actingUserId,
2830
workspaceId,
2931
workflowId,
32+
executionId,
3033
callerLabel = 'vertex',
3134
}: ResolveVertexCredentialParams): Promise<string> {
3235
const requestId = `${callerLabel}-${Date.now()}`
@@ -57,6 +60,15 @@ export async function resolveVertexCredential({
5760
const accessToken = await getServiceAccountToken(cred.id, [
5861
'https://www.googleapis.com/auth/cloud-platform',
5962
])
63+
recordCredentialAccess({
64+
actorId: actingUserId,
65+
workspaceId: cred.workspaceId,
66+
resourceId: cred.id,
67+
providerId: cred.providerId,
68+
credentialType: 'service_account',
69+
workflowId,
70+
executionId,
71+
})
6072
logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`)
6173
return accessToken
6274
}
@@ -70,6 +82,7 @@ export async function resolveVertexCredential({
7082
credentialId,
7183
userId: actingUserId,
7284
workflowId,
85+
executionId,
7386
toolLabel: 'Vertex AI',
7487
})
7588

‎apps/sim/lib/oauth/token-resolution.ts‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export interface ResolveCredentialTokenInput {
5858
requestId: string
5959
credentialId?: string
6060
workflowId?: string
61+
/** Workflow run the token is for; recorded on the credential-access audit entry. */
62+
executionId?: string
6163
/** Canonical provider scopes, used only by service-account token minting. */
6264
scopes?: string[]
6365
/** Google domain-wide-delegation subject for service-account credentials. */
@@ -112,9 +114,12 @@ export function recordCredentialAccess(params: {
112114
resourceId: string
113115
providerId: string | null | undefined
114116
credentialType: 'oauth' | 'service_account'
117+
workflowId?: string
118+
executionId?: string
115119
auditRequest?: CredentialAuditRequest
116120
}): void {
117-
const { actorId, workspaceId, resourceId, providerId, credentialType } = params
121+
const { actorId, workspaceId, resourceId, providerId, credentialType, workflowId, executionId } =
122+
params
118123
const credentialLabel = `${credentialType === 'oauth' ? 'OAuth' : 'service account'} credential for provider ${providerId ?? 'unknown'}`
119124
const audit = {
120125
workspaceId,
@@ -126,6 +131,8 @@ export function recordCredentialAccess(params: {
126131
metadata: {
127132
provider: providerId,
128133
credentialType,
134+
...(workflowId ? { workflowId } : {}),
135+
...(executionId ? { executionId } : {}),
129136
},
130137
}
131138
if (params.auditRequest) {
@@ -211,9 +218,20 @@ export async function completeOAuthCredentialToken(params: {
211218
resolvedCredentialId: string
212219
actorId?: string
213220
workspaceId: string | null
221+
workflowId?: string
222+
executionId?: string
214223
auditRequest?: CredentialAuditRequest
215224
}): Promise<ResolveCredentialTokenResult> {
216-
const { requestId, credential, resolvedCredentialId, actorId, workspaceId, auditRequest } = params
225+
const {
226+
requestId,
227+
credential,
228+
resolvedCredentialId,
229+
actorId,
230+
workspaceId,
231+
workflowId,
232+
executionId,
233+
auditRequest,
234+
} = params
217235
const contextValidation = validateOAuthCredentialContext(credential)
218236
if (!contextValidation.ok) {
219237
return { ok: false, status: 401, error: contextValidation.error }
@@ -229,6 +247,8 @@ export async function completeOAuthCredentialToken(params: {
229247
resourceId: resolvedCredentialId,
230248
providerId: credential.providerId,
231249
credentialType: 'oauth',
250+
workflowId,
251+
executionId,
232252
auditRequest,
233253
})
234254
}
@@ -301,6 +321,7 @@ export async function resolveCredentialToken(
301321
requestId,
302322
credentialId,
303323
workflowId,
324+
executionId,
304325
scopes,
305326
impersonateEmail,
306327
callerUserId,
@@ -355,6 +376,8 @@ export async function resolveCredentialToken(
355376
resourceId: resolved.credentialId,
356377
providerId: resolved.providerId,
357378
credentialType: 'service_account',
379+
workflowId,
380+
executionId,
358381
auditRequest,
359382
})
360383
}
@@ -433,6 +456,8 @@ export async function resolveCredentialToken(
433456
resolvedCredentialId,
434457
actorId: authz.requesterUserId,
435458
workspaceId: authz.workspaceId ?? null,
459+
workflowId,
460+
executionId,
436461
auditRequest,
437462
})
438463
} catch (error) {
@@ -477,6 +502,7 @@ export async function resolveCredentialAccessToken(
477502
requestId,
478503
credentialId,
479504
workflowId: input.workflowId,
505+
executionId: input.executionId,
480506
scopes: input.scopes,
481507
/**
482508
* In-process callers forward raw subblock state, where an untouched

‎apps/sim/tools/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,7 @@ async function executeToolImplementation(
19311931
credentialId,
19321932
userId,
19331933
workflowId,
1934+
executionId: scope.executionId,
19341935
toolId,
19351936
toolLabel,
19361937
scopes: providerScopes,

‎packages/audit/src/rollup.ts‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ const ROLLUP_MAX_ENTRIES = 100
1313
/** Pending rollups held by the process; reaching it writes every one of them. */
1414
const ROLLUP_MAX_PENDING = 1_000
1515

16-
/** Rows inserted at once by one write, so a large flush never floods the pool. */
16+
/**
17+
* Rollup rows the process inserts at once, across every window timer, size
18+
* flush, and scope end, so a burst of distinct rollups never floods the pool.
19+
*/
1720
const ROLLUP_WRITE_CONCURRENCY = 10
1821

1922
export interface AuditRollupSummary {
@@ -67,6 +70,8 @@ interface PendingRollup {
6770
}
6871

6972
const pending = new Map<string, PendingRollup>()
73+
const queuedWrites: Array<{ rollup: PendingRollup; written: () => void }> = []
74+
let activeWrites = 0
7075
const scopeStorage = new AsyncLocalStorage<RollupScope>()
7176
let nextScopeId = 0
7277

@@ -196,11 +201,31 @@ function toAuditParams(rollup: PendingRollup): AuditLogParams {
196201

197202
/**
198203
* One insert per row, so a row that cannot be written never takes others with
199-
* it, at most {@link ROLLUP_WRITE_CONCURRENCY} at a time.
204+
* it. Rows wait in one process-wide queue, at most
205+
* {@link ROLLUP_WRITE_CONCURRENCY} in flight.
200206
*/
201-
async function writePending(rollups: PendingRollup[]): Promise<void> {
202-
for (let index = 0; index < rollups.length; index += ROLLUP_WRITE_CONCURRENCY) {
203-
await Promise.all(rollups.slice(index, index + ROLLUP_WRITE_CONCURRENCY).map(writeRollup))
207+
function writePending(rollups: PendingRollup[]): Promise<void> {
208+
return Promise.all(
209+
rollups.map(
210+
(rollup) =>
211+
new Promise<void>((written) => {
212+
queuedWrites.push({ rollup, written })
213+
drainWrites()
214+
})
215+
)
216+
).then(() => undefined)
217+
}
218+
219+
function drainWrites(): void {
220+
while (activeWrites < ROLLUP_WRITE_CONCURRENCY) {
221+
const next = queuedWrites.shift()
222+
if (!next) return
223+
activeWrites += 1
224+
void writeRollup(next.rollup).finally(() => {
225+
activeWrites -= 1
226+
next.written()
227+
drainWrites()
228+
})
204229
}
205230
}
206231

0 commit comments

Comments
 (0)