mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
Fix remote import and rotation guards
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7aecdeb57b
commit
f6fc0f3e04
2 files changed
+78
No files matched your search
@@ -306,6 +306,59 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
expect(recreated.key).toBe("reusable-key");
|
||||
});
|
||||
|
||||
it("allows re-importing a remote secret after the prior external reference is soft-deleted", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const awsVault = await svc.createProviderConfig(companyId, {
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS production",
|
||||
config: { region: "us-east-1", namespace: "prod-use1" },
|
||||
});
|
||||
const externalRef =
|
||||
"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/reimportable";
|
||||
const deleted = await svc.create(companyId, {
|
||||
name: "Deleted external",
|
||||
key: "deleted-external",
|
||||
provider: "aws_secrets_manager",
|
||||
providerConfigId: awsVault.id,
|
||||
managedMode: "external_reference",
|
||||
externalRef,
|
||||
});
|
||||
|
||||
await svc.update(deleted.id, { status: "deleted" });
|
||||
vi.spyOn(awsSecretsManagerProvider, "listRemoteSecrets").mockResolvedValue({
|
||||
secrets: [
|
||||
{
|
||||
externalRef,
|
||||
name: "prod/reimportable",
|
||||
providerVersionRef: null,
|
||||
metadata: { arn: externalRef },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const preview = await svc.previewRemoteImport(companyId, {
|
||||
providerConfigId: awsVault.id,
|
||||
});
|
||||
const result = await svc.importRemoteSecrets(companyId, {
|
||||
providerConfigId: awsVault.id,
|
||||
secrets: [
|
||||
{
|
||||
externalRef,
|
||||
name: "Reimported external",
|
||||
key: "reimported-external",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preview.candidates[0]).toMatchObject({
|
||||
status: "ready",
|
||||
importable: true,
|
||||
conflicts: [],
|
||||
});
|
||||
expect(result).toMatchObject({ importedCount: 1, skippedCount: 0, errorCount: 0 });
|
||||
});
|
||||
|
||||
it("rejects provider vaults from another company when creating a secret", async () => {
|
||||
const companyA = await seedCompany("A");
|
||||
const companyB = await seedCompany("B");
|
||||
@@ -412,6 +465,28 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
expect(JSON.stringify(resolveSpy.mock.calls[0]?.[0])).not.toContain("resolved-secret");
|
||||
});
|
||||
|
||||
it("rejects rotation for non-active secrets", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const secret = await svc.create(companyId, {
|
||||
name: `disabled-rotation-${randomUUID()}`,
|
||||
provider: "local_encrypted",
|
||||
value: "runtime-secret",
|
||||
});
|
||||
|
||||
await svc.update(secret.id, { status: "disabled" });
|
||||
await expect(svc.rotate(secret.id, { value: "rotated-runtime-secret" })).rejects.toThrow(
|
||||
/non-active/i,
|
||||
);
|
||||
|
||||
const stored = await db
|
||||
.select({ latestVersion: companySecrets.latestVersion })
|
||||
.from(companySecrets)
|
||||
.where(eq(companySecrets.id, secret.id))
|
||||
.then((rows) => rows[0]);
|
||||
expect(stored?.latestVersion).toBe(1);
|
||||
});
|
||||
|
||||
it("previews AWS remote import candidates with duplicate and collision enrichment", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
|
||||
@@ -772,6 +772,7 @@ export function secretService(db: Db) {
|
||||
provider: companySecrets.provider,
|
||||
providerConfigId: companySecrets.providerConfigId,
|
||||
externalRef: companySecrets.externalRef,
|
||||
status: companySecrets.status,
|
||||
})
|
||||
.from(companySecrets)
|
||||
.where(eq(companySecrets.companyId, companyId));
|
||||
@@ -780,6 +781,7 @@ export function secretService(db: Db) {
|
||||
existingSecrets
|
||||
.filter((secret) =>
|
||||
secret.provider === provider &&
|
||||
secret.status !== "deleted" &&
|
||||
typeof secret.externalRef === "string" &&
|
||||
secret.externalRef.trim()
|
||||
)
|
||||
@@ -1535,6 +1537,7 @@ export function secretService(db: Db) {
|
||||
) => {
|
||||
const secret = await getById(secretId);
|
||||
if (!secret) throw notFound("Secret not found");
|
||||
if (secret.status !== "active") throw unprocessable("Cannot rotate a non-active secret");
|
||||
const providerId = secret.provider as SecretProvider;
|
||||
const provider = getSecretProvider(providerId);
|
||||
const providerConfigId =
|
||||
|
||||
Reference in new issue
Block a user