Implement provider vault data model and API

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-07 18:59:05 -05:00
1 parent b84482de60
commit ef1f56ef4c
16 files changed
+1026 -15

No files matched your search

+124 -11
View File
@@ -7,6 +7,13 @@ import { errorHandler } from "../middleware/error-handler.js";
const mockSecretService = vi.hoisted(() => ({
listProviders: vi.fn(),
checkProviders: vi.fn(),
listProviderConfigs: vi.fn(),
getProviderConfigById: vi.fn(),
createProviderConfig: vi.fn(),
updateProviderConfig: vi.fn(),
disableProviderConfig: vi.fn(),
setDefaultProviderConfig: vi.fn(),
checkProviderConfigHealth: vi.fn(),
create: vi.fn(),
}));
const mockLogActivity = vi.hoisted(() => vi.fn());
@@ -16,17 +23,17 @@ vi.mock("../services/index.js", () => ({
logActivity: mockLogActivity,
}));
function createApp() {
function createApp(actor: Record<string, unknown> = {
type: "board",
userId: "user-1",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "admin" }],
}) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
(req as any).actor = {
type: "board",
userId: "user-1",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "admin" }],
};
(req as any).actor = actor;
next();
});
app.use("/api", secretRoutes({} as any));
@@ -36,9 +43,9 @@ function createApp() {
describe("secret routes", () => {
beforeEach(() => {
mockSecretService.listProviders.mockReset();
mockSecretService.checkProviders.mockReset();
mockSecretService.create.mockReset();
for (const mock of Object.values(mockSecretService)) {
mock.mockReset();
}
mockLogActivity.mockReset();
});
@@ -79,4 +86,110 @@ describe("secret routes", () => {
expect(JSON.stringify(res.body)).toMatch(/Managed secrets cannot set externalRef/);
expect(mockSecretService.create).not.toHaveBeenCalled();
});
it("rejects provider vault routes for non-board actors", async () => {
const res = await request(createApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
})).get("/api/companies/company-1/secret-provider-configs");
expect(res.status).toBe(403);
expect(mockSecretService.listProviderConfigs).not.toHaveBeenCalled();
});
it("rejects provider vault cross-company access before calling the service", async () => {
const res = await request(createApp({
type: "board",
userId: "user-1",
source: "session",
companyIds: ["company-2"],
memberships: [{ companyId: "company-2", status: "active", membershipRole: "admin" }],
})).get("/api/companies/company-1/secret-provider-configs");
expect(res.status).toBe(403);
expect(mockSecretService.listProviderConfigs).not.toHaveBeenCalled();
});
it("rejects sensitive provider vault config fields", async () => {
const res = await request(createApp()).post("/api/companies/company-1/secret-provider-configs").send({
provider: "aws_secrets_manager",
displayName: "AWS prod",
config: {
region: "us-east-1",
accessKeyId: "AKIA...",
},
});
expect(res.status).toBe(400);
expect(JSON.stringify(res.body)).toMatch(/sensitive field/i);
expect(mockSecretService.createProviderConfig).not.toHaveBeenCalled();
});
it("rejects ready status for coming-soon provider vaults", async () => {
const res = await request(createApp()).post("/api/companies/company-1/secret-provider-configs").send({
provider: "vault",
displayName: "Vault draft",
status: "ready",
config: {
address: "https://vault.example.com",
},
});
expect(res.status).toBe(400);
expect(JSON.stringify(res.body)).toMatch(/locked while coming soon/i);
expect(mockSecretService.createProviderConfig).not.toHaveBeenCalled();
});
it("creates provider vaults and logs safe activity details", async () => {
const createdAt = new Date("2026-05-06T00:00:00.000Z");
mockSecretService.createProviderConfig.mockResolvedValue({
id: "11111111-1111-4111-8111-111111111111",
companyId: "company-1",
provider: "aws_secrets_manager",
displayName: "AWS prod",
status: "ready",
isDefault: true,
config: { region: "us-east-1" },
healthStatus: null,
healthCheckedAt: null,
healthMessage: null,
healthDetails: null,
disabledAt: null,
createdByAgentId: null,
createdByUserId: "user-1",
createdAt,
updatedAt: createdAt,
});
const res = await request(createApp()).post("/api/companies/company-1/secret-provider-configs").send({
provider: "aws_secrets_manager",
displayName: "AWS prod",
isDefault: true,
config: { region: "us-east-1" },
});
expect(res.status).toBe(201);
expect(mockSecretService.createProviderConfig).toHaveBeenCalledWith(
"company-1",
{
provider: "aws_secrets_manager",
displayName: "AWS prod",
status: undefined,
isDefault: true,
config: { region: "us-east-1" },
},
{ userId: "user-1", agentId: null },
);
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
action: "secret_provider_config.created",
details: {
provider: "aws_secrets_manager",
displayName: "AWS prod",
status: "ready",
isDefault: true,
},
}));
expect(JSON.stringify(mockLogActivity.mock.calls)).not.toContain("accessKey");
});
});
+65 -1
View File
@@ -7,6 +7,7 @@ import { eq } from "drizzle-orm";
import {
companies,
companySecretBindings,
companySecretProviderConfigs,
companySecretVersions,
companySecrets,
createDb,
@@ -35,7 +36,7 @@ describeEmbeddedPostgres("secretService", () => {
mkdirSync(secretsTmpDir, { recursive: true });
process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = path.join(secretsTmpDir, "master.key");
const started = await startEmbeddedPostgresTestDatabase("secrets-service");
stopDb = started.stop;
stopDb = started.cleanup;
db = createDb(started.connectionString);
});
@@ -45,6 +46,7 @@ describeEmbeddedPostgres("secretService", () => {
await db.delete(companySecretBindings);
await db.delete(companySecretVersions);
await db.delete(companySecrets);
await db.delete(companySecretProviderConfigs);
await db.delete(companies);
});
@@ -191,6 +193,68 @@ describeEmbeddedPostgres("secretService", () => {
).rejects.toThrow(/not bound/i);
});
it("keeps one default provider vault per company provider", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const first = await svc.createProviderConfig(companyId, {
provider: "local_encrypted",
displayName: "Local primary",
isDefault: true,
config: {},
});
const second = await svc.createProviderConfig(companyId, {
provider: "local_encrypted",
displayName: "Local secondary",
isDefault: true,
config: {},
});
const rows = await svc.listProviderConfigs(companyId);
expect(rows.find((row) => row.id === first.id)?.isDefault).toBe(false);
expect(rows.find((row) => row.id === second.id)?.isDefault).toBe(true);
});
it("rejects provider vaults from another company when creating a secret", async () => {
const companyA = await seedCompany("A");
const companyB = await seedCompany("B");
const svc = secretService(db);
const foreignVault = await svc.createProviderConfig(companyB, {
provider: "local_encrypted",
displayName: "Foreign vault",
config: {},
});
await expect(
svc.create(companyA, {
name: `managed-${randomUUID()}`,
provider: "local_encrypted",
providerConfigId: foreignVault.id,
value: "runtime-secret",
}),
).rejects.toThrow(/same company/i);
});
it("blocks coming-soon provider vaults from secret selection", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const draftVault = await svc.createProviderConfig(companyId, {
provider: "gcp_secret_manager",
displayName: "GCP draft",
config: { projectId: "paperclip-prod1" },
});
expect(draftVault.status).toBe("coming_soon");
await expect(
svc.create(companyId, {
name: `draft-${randomUUID()}`,
provider: "gcp_secret_manager",
providerConfigId: draftVault.id,
value: "runtime-secret",
}),
).rejects.toThrow(/coming soon/i);
});
it("rejects externalRef overrides on managed secrets", async () => {
const companyId = await seedCompany();
const svc = secretService(db);