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 <noreply@paperclip.ing>
This commit is contained in:
Paperclip Codex 2026-09-12 08:01:29 +00:00
parent a25aff0516
commit c667b9d913
2 changed files with 30 additions and 33 deletions

View File

@ -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",

View File

@ -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", {