From c667b9d913a89322dd746a6f00ebe540ab5eb0c7 Mon Sep 17 00:00:00 2001 From: Paperclip Codex Date: Sat, 12 Sep 2026 08:01:29 +0000 Subject: [PATCH] fix(secrets): expose low-trust user denials in audit log Associate value-free low-trust denial events with the configured user-secret record so the existing audit API can retrieve them.\n\nCo-Authored-By: Paperclip --- server/src/__tests__/secrets-service.test.ts | 13 ++--- server/src/services/secrets.ts | 50 ++++++++++---------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/server/src/__tests__/secrets-service.test.ts b/server/src/__tests__/secrets-service.test.ts index 2da7ab937c..2d6eea9cb0 100644 --- a/server/src/__tests__/secrets-service.test.ts +++ b/server/src/__tests__/secrets-service.test.ts @@ -1686,7 +1686,7 @@ describeEmbeddedPostgres("secretService", () => { ROUTINE_PLAIN: "still-here", }; await svc.syncEnvBindingsForTarget(companyId, { targetType: "routine", targetId: "routine-1" }, env); - await svc.createCurrentUserSecretValue(companyId, "user-1", { + const userSecret = await svc.createCurrentUserSecretValue(companyId, "user-1", { definitionKey: "github_token", value: "user-one-secret", }); @@ -1709,15 +1709,10 @@ describeEmbeddedPostgres("secretService", () => { expect(resolved.secretKeys.has("GITHUB_TOKEN")).toBe(false); expect(JSON.stringify(resolved)).not.toContain("user-one-secret"); - const [denialEvent] = await db - .select() - .from(secretAccessEvents) - .where(and( - eq(secretAccessEvents.companyId, companyId), - eq(secretAccessEvents.userSecretDefinitionId, definition.id), - )); + const [denialEvent] = await svc.listAccessEvents(companyId, userSecret.id); expect(denialEvent).toMatchObject({ - secretId: null, + secretId: userSecret.id, + userSecretDefinitionId: definition.id, secretScope: "user", responsibleUserId: "user-1", credentialOwnerUserId: "user-1", diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index db66325b22..fd2f9c0626 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -1157,7 +1157,7 @@ export function secretService(db: Db | DbTransaction) { async function recordAccessEvent(input: { companyId: string; - secretId: string | null; + secretId: string; userSecretDefinitionId?: string | null; secretScope?: string | null; version: number | null; @@ -4164,34 +4164,36 @@ export function secretService(db: Db | DbTransaction) { ); } } - if ( - Array.isArray(context?.allowedBindingIds) && - (!declaration || !context.allowedBindingIds.includes(declaration.id)) - ) { - await recordAccessEvent({ - companyId, - secretId: null, - userSecretDefinitionId: definition.id, - secretScope: "user", - version: null, - provider: definition.provider as SecretProvider, - context: context ? { ...context, responsibleUserId } : undefined, - credentialOwnerUserId: responsibleUserId, - credentialSubjectType: "user", - credentialSubjectId: responsibleUserId, - outcome: "failure", - errorCode: "binding_not_allowed", - }).catch(() => undefined); - throw unprocessable( - "User secret declaration is outside the active low-trust boundary", - { code: "binding_not_allowed" }, - ); - } const secret = await getUserSecretValue({ companyId, ownerUserId: responsibleUserId, definitionId: definition.id, }); + if ( + Array.isArray(context?.allowedBindingIds) && + (!declaration || !context.allowedBindingIds.includes(declaration.id)) + ) { + if (secret) { + await recordAccessEvent({ + companyId, + secretId: secret.id, + userSecretDefinitionId: definition.id, + secretScope: "user", + version: null, + provider: definition.provider as SecretProvider, + context: context ? { ...context, responsibleUserId } : undefined, + credentialOwnerUserId: responsibleUserId, + credentialSubjectType: "user", + credentialSubjectId: responsibleUserId, + outcome: "failure", + errorCode: "binding_not_allowed", + }).catch(() => undefined); + } + throw unprocessable( + "User secret declaration is outside the active low-trust boundary", + { code: "binding_not_allowed" }, + ); + } if (!secret) { if (optionalBinding) return null; throw unprocessable("User secret value is not configured", {