diff --git a/server/src/__tests__/secrets-service.test.ts b/server/src/__tests__/secrets-service.test.ts index 69ba8824f97..e0f6f319175 100644 --- a/server/src/__tests__/secrets-service.test.ts +++ b/server/src/__tests__/secrets-service.test.ts @@ -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); diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index f8a81187de6..0d3f721d6fc 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -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 =