mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Enforce low-trust allowlist for user secrets
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4fb2d46cb5
commit
1a0c7bee75
2 files changed
+69
-2
No files matched your search
@@ -346,6 +346,56 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
expect(resolved.manifest[0]?.bindingId).toBe(binding!.id);
|
||||
});
|
||||
|
||||
it("denies user secret resolution outside the low-trust declaration allowlist", async () => {
|
||||
const companyId = await seedCompany();
|
||||
await seedCompanyMember(companyId, "user-1", "owner");
|
||||
const svc = secretService(db);
|
||||
const definition = await svc.createUserSecretDefinition(companyId, {
|
||||
key: "github_token",
|
||||
name: "GitHub token",
|
||||
provider: "local_encrypted",
|
||||
});
|
||||
const env = {
|
||||
GITHUB_TOKEN: { type: "user_secret_ref" as const, key: "github_token", version: "latest" as const },
|
||||
};
|
||||
|
||||
await svc.syncEnvBindingsForTarget(companyId, { targetType: "agent", targetId: "agent-1" }, env);
|
||||
await svc.createCurrentUserSecretValue(companyId, "user-1", {
|
||||
definitionKey: "github_token",
|
||||
value: "user-one-secret",
|
||||
});
|
||||
const [declaration] = await db
|
||||
.select()
|
||||
.from(userSecretDeclarations)
|
||||
.where(eq(userSecretDeclarations.userSecretDefinitionId, definition.id));
|
||||
expect(declaration?.id).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
svc.resolveEnvBindings(companyId, env, {
|
||||
consumerType: "agent",
|
||||
consumerId: "agent-1",
|
||||
actorType: "agent",
|
||||
actorId: "agent-1",
|
||||
responsibleUserId: "user-1",
|
||||
allowedBindingIds: ["11111111-1111-4111-8111-111111111111"],
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
status: 422,
|
||||
details: { code: "binding_not_allowed" },
|
||||
});
|
||||
|
||||
const resolved = await svc.resolveEnvBindings(companyId, env, {
|
||||
consumerType: "agent",
|
||||
consumerId: "agent-1",
|
||||
actorType: "agent",
|
||||
actorId: "agent-1",
|
||||
responsibleUserId: "user-1",
|
||||
allowedBindingIds: [declaration!.id],
|
||||
});
|
||||
expect(resolved.env.GITHUB_TOKEN).toBe("user-one-secret");
|
||||
expect(resolved.manifest[0]?.bindingId).toBe(declaration!.id);
|
||||
});
|
||||
|
||||
it("resolves routine env secret refs through routine bindings and records value-free access metadata", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
|
||||
@@ -2262,8 +2262,9 @@ export function secretService(db: Db) {
|
||||
code: "responsible_user_missing",
|
||||
});
|
||||
}
|
||||
let declaration: typeof userSecretDeclarations.$inferSelect | null = null;
|
||||
if (context?.configPath) {
|
||||
const declaration = await db
|
||||
declaration = await db
|
||||
.select()
|
||||
.from(userSecretDeclarations)
|
||||
.where(and(
|
||||
@@ -2281,6 +2282,15 @@ export function secretService(db: Db) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
Array.isArray(context?.allowedBindingIds) &&
|
||||
(!declaration || !context.allowedBindingIds.includes(declaration.id))
|
||||
) {
|
||||
throw unprocessable(
|
||||
"User secret declaration is outside the active low-trust boundary",
|
||||
{ code: "binding_not_allowed" },
|
||||
);
|
||||
}
|
||||
const secret = await getUserSecretValue({
|
||||
companyId,
|
||||
ownerUserId: responsibleUserId,
|
||||
@@ -2294,7 +2304,7 @@ export function secretService(db: Db) {
|
||||
responsibleUserId,
|
||||
});
|
||||
}
|
||||
return await resolveSecretValueInternal(
|
||||
const resolution = await resolveSecretValueInternal(
|
||||
companyId,
|
||||
secret.id,
|
||||
input.version ?? "latest",
|
||||
@@ -2303,6 +2313,13 @@ export function secretService(db: Db) {
|
||||
allowUserSecretScope: true,
|
||||
},
|
||||
);
|
||||
return {
|
||||
...resolution,
|
||||
manifestEntry: {
|
||||
...resolution.manifestEntry,
|
||||
bindingId: declaration?.id ?? resolution.manifestEntry.bindingId ?? null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
previewRemoteImport: async (
|
||||
|
||||
Reference in new issue
Block a user