diff --git a/backend/e2e-test/routes/v1/secret-approval-policy.spec.ts b/backend/e2e-test/routes/v1/secret-approval-policy.spec.ts new file mode 100644 index 000000000..3234503e5 --- /dev/null +++ b/backend/e2e-test/routes/v1/secret-approval-policy.spec.ts @@ -0,0 +1,35 @@ +import { seedData1 } from "@app/db/seed-data"; + +const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/secret-approvals`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + name: dto.name, + secretPath: dto.secretPath, + approvers: dto.approvers, + approvals: dto.approvals + } + }); + + expect(res.statusCode).toBe(200); + return res.json().approval; +}; + +describe("Secret approval policy router", async () => { + test("Create policy", async () => { + const policy = await createPolicy({ + secretPath: "/", + approvals: 1, + approvers: [seedData1.id], + name: "test-policy" + }); + + expect(policy.name).toBe("test-policy"); + }); +}); diff --git a/backend/e2e-test/routes/v1/secret-import.spec.ts b/backend/e2e-test/routes/v1/secret-import.spec.ts index c184e44e5..dd4f9c4ca 100644 --- a/backend/e2e-test/routes/v1/secret-import.spec.ts +++ b/backend/e2e-test/routes/v1/secret-import.spec.ts @@ -1,73 +1,61 @@ +import { createFolder, deleteFolder } from "e2e-test/testUtils/folders"; +import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports"; +import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets"; + import { seedData1 } from "@app/db/seed-data"; -const createSecretImport = async (importPath: string, importEnv: string) => { - const res = await testServer.inject({ - method: "POST", - url: `/api/v1/secret-imports`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - }, - body: { - workspaceId: seedData1.project.id, - environment: seedData1.environment.slug, - path: "/", - import: { - environment: importEnv, - path: importPath - } - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("secretImport"); - return payload.secretImport; -}; - -const deleteSecretImport = async (id: string) => { - const res = await testServer.inject({ - method: "DELETE", - url: `/api/v1/secret-imports/${id}`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - }, - body: { - workspaceId: seedData1.project.id, - environment: seedData1.environment.slug, - path: "/" - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("secretImport"); - return payload.secretImport; -}; - describe("Secret Import Router", async () => { test.each([ { importEnv: "prod", importPath: "/" }, // one in root { importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones ])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => { // check for default environments - const payload = await createSecretImport(importPath, importEnv); + const payload = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath, + importEnv + }); expect(payload).toEqual( expect.objectContaining({ id: expect.any(String), - importPath: expect.any(String), + importPath, importEnv: expect.objectContaining({ name: expect.any(String), - slug: expect.any(String), + slug: importEnv, id: expect.any(String) }) }) ); - await deleteSecretImport(payload.id); + + await deleteSecretImport({ + id: payload.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); test("Get secret imports", async () => { - const createdImport1 = await createSecretImport("/", "prod"); - const createdImport2 = await createSecretImport("/", "staging"); + const createdImport1 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "prod" + }); + const createdImport2 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "staging" + }); const res = await testServer.inject({ method: "GET", url: `/api/v1/secret-imports`, @@ -89,25 +77,60 @@ describe("Secret Import Router", async () => { expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), - importPath: expect.any(String), + importPath: "/", importEnv: expect.objectContaining({ name: expect.any(String), - slug: expect.any(String), + slug: "prod", + id: expect.any(String) + }) + }), + expect.objectContaining({ + id: expect.any(String), + importPath: "/", + importEnv: expect.objectContaining({ + name: expect.any(String), + slug: "staging", id: expect.any(String) }) }) ]) ); - await deleteSecretImport(createdImport1.id); - await deleteSecretImport(createdImport2.id); + await deleteSecretImport({ + id: createdImport1.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); + await deleteSecretImport({ + id: createdImport2.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); test("Update secret import position", async () => { const prodImportDetails = { path: "/", envSlug: "prod" }; const stagingImportDetails = { path: "/", envSlug: "staging" }; - const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug); - const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug); + const createdImport1 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: prodImportDetails.path, + importEnv: prodImportDetails.envSlug + }); + const createdImport2 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: stagingImportDetails.path, + importEnv: stagingImportDetails.envSlug + }); const updateImportRes = await testServer.inject({ method: "PATCH", @@ -161,22 +184,55 @@ describe("Secret Import Router", async () => { expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id); expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id); - await deleteSecretImport(createdImport1.id); - await deleteSecretImport(createdImport2.id); + await deleteSecretImport({ + id: createdImport1.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); + await deleteSecretImport({ + id: createdImport2.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); test("Delete secret import position", async () => { - const createdImport1 = await createSecretImport("/", "prod"); - const createdImport2 = await createSecretImport("/", "staging"); - const deletedImport = await deleteSecretImport(createdImport1.id); + const createdImport1 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "prod" + }); + const createdImport2 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "staging" + }); + const deletedImport = await deleteSecretImport({ + id: createdImport1.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); + // check for default environments expect(deletedImport).toEqual( expect.objectContaining({ id: expect.any(String), - importPath: expect.any(String), + importPath: "/", importEnv: expect.objectContaining({ name: expect.any(String), - slug: expect.any(String), + slug: "prod", id: expect.any(String) }) }) @@ -201,6 +257,552 @@ describe("Secret Import Router", async () => { expect(secretImportList.secretImports.length).toEqual(1); expect(secretImportList.secretImports[0].position).toEqual(1); - await deleteSecretImport(createdImport2.id); + await deleteSecretImport({ + id: createdImport2.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); }); + +// dev <- stage <- prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret import waterfall pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging" + }); + + const stageImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod" + }); + + return async () => { + await deleteSecretImport({ + id: stageImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging", + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check one level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + }); + + test("Check two level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + + expect(secret.secretKey).toBe("PROD_KEY"); + expect(secret.secretValue).toBe("prod-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + } +); + +// dev <- stage, dev <- prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret import multiple destination to one source pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging" + }); + + const devImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod" + }); + + return async () => { + await deleteSecretImport({ + id: devImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }), + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + } +); + +// dev -> stage, prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret import one source to multiple destination pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const stageImportFromDev = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: seedData1.environment.slug + }); + + const prodImportFromDev = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: seedData1.environment.slug + }); + + return async () => { + await deleteSecretImport({ + id: prodImportFromDev.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod", + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: stageImportFromDev.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging", + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check imported secret exist", async () => { + await createSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + await createSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + const stagingSecret = await getSecretByNameV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(stagingSecret.secretKey).toBe("STAGING_KEY"); + expect(stagingSecret.secretValue).toBe("stage-value"); + + const prodSecret = await getSecretByNameV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + + expect(prodSecret.secretKey).toBe("PROD_KEY"); + expect(prodSecret.secretValue).toBe("prod-value"); + + await deleteSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + await deleteSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + } +); diff --git a/backend/e2e-test/routes/v1/secret-replication.spec.ts b/backend/e2e-test/routes/v1/secret-replication.spec.ts new file mode 100644 index 000000000..f762165a2 --- /dev/null +++ b/backend/e2e-test/routes/v1/secret-replication.spec.ts @@ -0,0 +1,406 @@ +import { createFolder, deleteFolder } from "e2e-test/testUtils/folders"; +import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports"; +import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets"; + +import { seedData1 } from "@app/db/seed-data"; + +// dev <- stage <- prod +describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])( + "Secret replication waterfall pattern testing - %secretPath", + ({ secretPath: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging", + isReplication: true + }); + + const stageImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod", + isReplication: true + }); + + return async () => { + await deleteSecretImport({ + id: stageImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging", + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check one level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + // wait for 5 second for replication to finish + await new Promise((resolve) => { + setTimeout(resolve, 5000); // time to breathe for db + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + }); + + test("Check two level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + // wait for 5 second for replication to finish + await new Promise((resolve) => { + setTimeout(resolve, 5000); // time to breathe for db + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + + expect(secret.secretKey).toBe("PROD_KEY"); + expect(secret.secretValue).toBe("prod-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + }, + { timeout: 30000 } +); + +// dev <- stage, dev <- prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret replication 1-N pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging", + isReplication: true + }); + + const devImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod", + isReplication: true + }); + + return async () => { + await deleteSecretImport({ + id: devImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + // wait for 5 second for replication to finish + await new Promise((resolve) => { + setTimeout(resolve, 5000); // time to breathe for db + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }), + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + }, + { timeout: 30000 } +); diff --git a/backend/e2e-test/routes/v3/secret-reference.spec.ts b/backend/e2e-test/routes/v3/secret-reference.spec.ts new file mode 100644 index 000000000..fb23cdc25 --- /dev/null +++ b/backend/e2e-test/routes/v3/secret-reference.spec.ts @@ -0,0 +1,330 @@ +import { createFolder, deleteFolder } from "e2e-test/testUtils/folders"; +import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports"; +import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets"; + +import { seedData1 } from "@app/db/seed-data"; + +describe("Secret expansion", () => { + const projectId = seedData1.projectV3.id; + + beforeAll(async () => { + const prodRootFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/", + name: "deep" + }); + + await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep", + name: "nested" + }); + + return async () => { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodRootFolder.id, + workspaceId: projectId, + environmentSlug: "prod" + }); + }; + }); + + test("Local secret reference", async () => { + const secrets = [ + { + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "HELLO", + value: "world" + }, + { + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "TEST", + // eslint-disable-next-line + value: "hello ${HELLO}" + } + ]; + + await Promise.all(secrets.map((el) => createSecretV2(el))); + + const expandedSecret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "TEST" + }); + expect(expandedSecret.secretValue).toBe("hello world"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken + }); + expect(listSecrets.secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretKey: "TEST", + secretValue: "hello world" + }) + ]) + ); + + await Promise.all(secrets.map((el) => deleteSecretV2(el))); + }); + + test("Cross environment secret reference", async () => { + const secrets = [ + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep", + authToken: jwtAuthToken, + key: "DEEP_KEY_1", + value: "testing" + }, + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep/nested", + authToken: jwtAuthToken, + key: "NESTED_KEY_1", + value: "reference" + }, + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep/nested", + authToken: jwtAuthToken, + key: "NESTED_KEY_2", + // eslint-disable-next-line + value: "secret ${NESTED_KEY_1}" + }, + { + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "KEY", + // eslint-disable-next-line + value: "hello ${prod.deep.DEEP_KEY_1} ${prod.deep.nested.NESTED_KEY_2}" + } + ]; + + await Promise.all(secrets.map((el) => createSecretV2(el))); + + const expandedSecret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "KEY" + }); + expect(expandedSecret.secretValue).toBe("hello testing secret reference"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken + }); + expect(listSecrets.secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretKey: "KEY", + secretValue: "hello testing secret reference" + }) + ]) + ); + + await Promise.all(secrets.map((el) => deleteSecretV2(el))); + }); + + test("Non replicated secret import secret expansion on local reference and nested reference", async () => { + const secrets = [ + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep", + authToken: jwtAuthToken, + key: "DEEP_KEY_1", + value: "testing" + }, + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep/nested", + authToken: jwtAuthToken, + key: "NESTED_KEY_1", + value: "reference" + }, + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep/nested", + authToken: jwtAuthToken, + key: "NESTED_KEY_2", + // eslint-disable-next-line + value: "secret ${NESTED_KEY_1} ${prod.deep.DEEP_KEY_1}" + }, + { + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "KEY", + // eslint-disable-next-line + value: "hello world" + } + ]; + + await Promise.all(secrets.map((el) => createSecretV2(el))); + const secretImportFromProdToDev = await createSecretImport({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + importEnv: "prod", + importPath: "/deep/nested" + }); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretPath: "/deep/nested", + environment: "prod", + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "NESTED_KEY_1", + secretValue: "reference" + }), + expect.objectContaining({ + secretKey: "NESTED_KEY_2", + secretValue: "secret reference testing" + }) + ]) + }) + ]) + ); + + await Promise.all(secrets.map((el) => deleteSecretV2(el))); + await deleteSecretImport({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + authToken: jwtAuthToken, + id: secretImportFromProdToDev.id, + secretPath: "/" + }); + }); + + test( + "Replicated secret import secret expansion on local reference and nested reference", + async () => { + const secrets = [ + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep", + authToken: jwtAuthToken, + key: "DEEP_KEY_1", + value: "testing" + }, + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep/nested", + authToken: jwtAuthToken, + key: "NESTED_KEY_1", + value: "reference" + }, + { + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep/nested", + authToken: jwtAuthToken, + key: "NESTED_KEY_2", + // eslint-disable-next-line + value: "secret ${NESTED_KEY_1} ${prod.deep.DEEP_KEY_1}" + }, + { + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "KEY", + // eslint-disable-next-line + value: "hello world" + } + ]; + + await Promise.all(secrets.map((el) => createSecretV2(el))); + const secretImportFromProdToDev = await createSecretImport({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + importEnv: "prod", + importPath: "/deep/nested", + isReplication: true + }); + + // wait for 5 second for replication to finish + await new Promise((resolve) => { + setTimeout(resolve, 5000); // time to breathe for db + }); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretPath: `/__reserve_replication_${secretImportFromProdToDev.id}`, + environment: seedData1.environment.slug, + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "NESTED_KEY_1", + secretValue: "reference" + }), + expect.objectContaining({ + secretKey: "NESTED_KEY_2", + secretValue: "secret reference testing" + }) + ]) + }) + ]) + ); + + await Promise.all(secrets.map((el) => deleteSecretV2(el))); + await deleteSecretImport({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + authToken: jwtAuthToken, + id: secretImportFromProdToDev.id, + secretPath: "/" + }); + }, + { timeout: 10000 } + ); +}); diff --git a/backend/e2e-test/routes/v3/secrets-v2.spec.ts b/backend/e2e-test/routes/v3/secrets-v2.spec.ts index 70066c061..dc02587cd 100644 --- a/backend/e2e-test/routes/v3/secrets-v2.spec.ts +++ b/backend/e2e-test/routes/v3/secrets-v2.spec.ts @@ -8,6 +8,7 @@ type TRawSecret = { secretComment?: string; version: number; }; + const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => { const createSecretReqBody = { workspaceId: seedData1.projectV3.id, diff --git a/backend/e2e-test/testUtils/folders.ts b/backend/e2e-test/testUtils/folders.ts new file mode 100644 index 000000000..a5484a22e --- /dev/null +++ b/backend/e2e-test/testUtils/folders.ts @@ -0,0 +1,73 @@ +type TFolder = { + id: string; + name: string; +}; + +export const createFolder = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + name: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + name: dto.name, + path: dto.secretPath + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder as TFolder; +}; + +export const deleteFolder = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + id: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v1/folders/${dto.id}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder as TFolder; +}; + +export const listFolders = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + expect(res.statusCode).toBe(200); + return res.json().folders as TFolder[]; +}; diff --git a/backend/e2e-test/testUtils/secret-imports.ts b/backend/e2e-test/testUtils/secret-imports.ts new file mode 100644 index 000000000..370a8dfb8 --- /dev/null +++ b/backend/e2e-test/testUtils/secret-imports.ts @@ -0,0 +1,93 @@ +type TSecretImport = { + id: string; + importEnv: { + name: string; + slug: string; + id: string; + }; + importPath: string; +}; + +export const createSecretImport = async (dto: { + workspaceId: string; + environmentSlug: string; + isReplication?: boolean; + secretPath: string; + importPath: string; + importEnv: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/secret-imports`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + isReplication: dto.isReplication, + path: dto.secretPath, + import: { + environment: dto.importEnv, + path: dto.importPath + } + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImport"); + return payload.secretImport as TSecretImport; +}; + +export const deleteSecretImport = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; + id: string; +}) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v1/secret-imports/${dto.id}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImport"); + return payload.secretImport as TSecretImport; +}; + +export const listSecretImport = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v1/secret-imports`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + query: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImports"); + return payload.secretImports as TSecretImport[]; +}; diff --git a/backend/e2e-test/testUtils/secrets.ts b/backend/e2e-test/testUtils/secrets.ts new file mode 100644 index 000000000..96ecc91c6 --- /dev/null +++ b/backend/e2e-test/testUtils/secrets.ts @@ -0,0 +1,128 @@ +import { SecretType } from "@app/db/schemas"; + +type TRawSecret = { + secretKey: string; + secretValue: string; + secretComment?: string; + version: number; +}; + +export const createSecretV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + key: string; + value: string; + comment?: string; + authToken: string; + type?: SecretType; +}) => { + const createSecretReqBody = { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + type: dto.type || SecretType.Shared, + secretPath: dto.secretPath, + secretKey: dto.key, + secretValue: dto.value, + secretComment: dto.comment + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secret"); + return createdSecretPayload.secret as TRawSecret; +}; + +export const deleteSecretV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + key: string; + authToken: string; +}) => { + const deleteSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + secretPath: dto.secretPath + } + }); + expect(deleteSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(deleteSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + return updatedSecretPayload.secret as TRawSecret; +}; + +export const getSecretByNameV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + key: string; + authToken: string; +}) => { + const response = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + query: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + secretPath: dto.secretPath, + expandSecretReferences: "true", + include_imports: "true" + } + }); + expect(response.statusCode).toBe(200); + const payload = JSON.parse(response.payload); + expect(payload).toHaveProperty("secret"); + return payload.secret as TRawSecret; +}; + +export const getSecretsV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; +}) => { + const getSecretsResponse = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/raw`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + query: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + secretPath: dto.secretPath, + expandSecretReferences: "true", + include_imports: "true" + } + }); + expect(getSecretsResponse.statusCode).toBe(200); + const getSecretsPayload = JSON.parse(getSecretsResponse.payload); + expect(getSecretsPayload).toHaveProperty("secrets"); + expect(getSecretsPayload).toHaveProperty("imports"); + return getSecretsPayload as { + secrets: TRawSecret[]; + imports: { + secretPath: string; + environment: string; + folderId: string; + secrets: TRawSecret[]; + }[]; + }; +}; diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index cc1f9afc2..7be0b860f 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -11,10 +11,11 @@ import { initLogger } from "@app/lib/logger"; import { main } from "@app/server/app"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; -import { mockQueue } from "./mocks/queue"; import { mockSmtpServer } from "./mocks/smtp"; -import { mockKeyStore } from "./mocks/keystore"; import { initDbConnection } from "@app/db"; +import { queueServiceFactory } from "@app/queue"; +import { keyStoreFactory } from "@app/keystore/keystore"; +import { Redis } from "ioredis"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { @@ -28,19 +29,31 @@ export default { dbRootCert: cfg.DB_ROOT_CERT }); + const redis = new Redis(cfg.REDIS_URL); + await redis.flushdb("SYNC"); + try { + await db.migrate.rollback( + { + directory: path.join(__dirname, "../src/db/migrations"), + extension: "ts", + tableName: "infisical_migrations" + }, + true + ); await db.migrate.latest({ directory: path.join(__dirname, "../src/db/migrations"), extension: "ts", tableName: "infisical_migrations" }); + await db.seed.run({ directory: path.join(__dirname, "../src/db/seeds"), extension: "ts" }); const smtp = mockSmtpServer(); - const queue = mockQueue(); - const keyStore = mockKeyStore(); + const queue = queueServiceFactory(cfg.REDIS_URL); + const keyStore = keyStoreFactory(cfg.REDIS_URL); const server = await main({ db, smtp, logger, queue, keyStore }); // @ts-expect-error type globalThis.testServer = server; @@ -58,10 +71,12 @@ export default { { expiresIn: cfg.JWT_AUTH_LIFETIME } ); } catch (error) { + // eslint-disable-next-line console.log("[TEST] Error setting up environment", error); await db.destroy(); throw error; } + // custom setup return { async teardown() { @@ -80,6 +95,9 @@ export default { }, true ); + + await redis.flushdb("ASYNC"); + redis.disconnect(); await db.destroy(); } }; diff --git a/backend/package-lock.json b/backend/package-lock.json index d860616f1..0a879d934 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -136,7 +136,6 @@ "tsup": "^8.0.1", "tsx": "^4.4.0", "typescript": "^5.3.2", - "vite-tsconfig-paths": "^4.2.2", "vitest": "^1.2.2" } }, @@ -301,16 +300,16 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.635.0.tgz", - "integrity": "sha512-M2SGf0B/WmHYNxUhUWKIYI5NW4Si7cyokB6Lt3RtDof3WVHA8L0LLl+EEo1URUkpxH8/F8VH2fTES7ODm2c+7g==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.632.0.tgz", + "integrity": "sha512-QrG04Ss2/KDsvGmoBH9QHjaC/wx7Gf9U2F5o8gYbHVU5ZGDW+zMX2Sj/6jjSyZ4qLD4sxK7sRHwK+fYA21OQQA==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.635.0", - "@aws-sdk/client-sts": "3.635.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.635.0", + "@aws-sdk/client-sso-oidc": "3.632.0", + "@aws-sdk/client-sts": "3.632.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", @@ -321,7 +320,7 @@ "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/eventstream-serde-browser": "^3.0.6", "@smithy/eventstream-serde-config-resolver": "^3.0.3", "@smithy/eventstream-serde-node": "^3.0.5", @@ -330,20 +329,20 @@ "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -561,6 +560,45 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/core": { + "version": "3.635.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.635.0.tgz", + "integrity": "sha512-i1x/E/sgA+liUE1XJ7rj1dhyXpAKO1UKFUcTTHXok2ARjWTvszHnSXMOsB77aPbmn0fUp1JTx2kHUAZ1LVt5Bg==", + "dependencies": { + "@smithy/core": "^2.4.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.635.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.635.0.tgz", + "integrity": "sha512-iJyRgEjOCQlBMXqtwPLIKYc7Bsc6nqjrZybdMDenPDa+kmLg7xh8LxHsu9088e+2/wtLicE34FsJJIfzu3L82g==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.637.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.637.0.tgz", @@ -659,16 +697,16 @@ } }, "node_modules/@aws-sdk/client-iam": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.635.0.tgz", - "integrity": "sha512-sflTv6XcwO5UX+U9x31+T6TBEgVIzG61giLgRV51kkFGErni++GpxUcc6O1mpDwb3jpbntJf7QPjrkkj9wsTPA==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.632.0.tgz", + "integrity": "sha512-iwivASUliVxCEbT/mu5s03SCyqQKNXbJUpG17ywT4taA2xvLisGRI5iNV3OYT1qDmK9DOLMSJYpeX2GWCijPxw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.635.0", - "@aws-sdk/client-sts": "3.635.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.635.0", + "@aws-sdk/client-sso-oidc": "3.632.0", + "@aws-sdk/client-sts": "3.632.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", @@ -679,26 +717,26 @@ "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -711,16 +749,16 @@ } }, "node_modules/@aws-sdk/client-kms": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.635.0.tgz", - "integrity": "sha512-H2qJVXiz3WbBQwtxqfEvuJ9pCKJdqEWkzQ8I4knkXbQkyy78GktfMwBWqFyw3eap/s9rQmsyXbuuhBIozbemOg==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.632.0.tgz", + "integrity": "sha512-uMm1fAIdImaBKwKXnpcD1cpRlTAbLisbRbNJqzJdH+snN0jAkukLNUMUheb0XKaczk7eQrp5w4inlWrRvEmjSA==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.635.0", - "@aws-sdk/client-sts": "3.635.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.635.0", + "@aws-sdk/client-sso-oidc": "3.632.0", + "@aws-sdk/client-sts": "3.632.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", @@ -731,26 +769,26 @@ "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -762,16 +800,16 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.635.0.tgz", - "integrity": "sha512-taa+sa8xFym7ZYzybqkOVy5MAdedcIt2pKEVOReEaNkUuOwMUo+wF4QhJeyhaLPTs2l0rHR1bnwYOG+0fW0Kvg==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.632.0.tgz", + "integrity": "sha512-WsQhPHHK1yPfALcP1B7nBSGDzky6vFTUEXnUdfzb5Xy2cT+JTBTS6ChtQGqqOuGHDP/3t/9soqZ+L6rUCYBb/Q==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.635.0", - "@aws-sdk/client-sts": "3.635.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.635.0", + "@aws-sdk/client-sso-oidc": "3.632.0", + "@aws-sdk/client-sts": "3.632.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", @@ -782,26 +820,26 @@ "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -814,13 +852,13 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.635.0.tgz", - "integrity": "sha512-/Hl69+JpFUo9JNVmh2gSvMgYkE4xjd+1okiRoPBbQqjI7YBP2JWCUDP8IoEkNq3wj0vNTq0OWfn6RpZycIkAXQ==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.632.0.tgz", + "integrity": "sha512-iYWHiKBz44m3chCFvtvHnvCpL2rALzyr1e6tOZV3dLlOKtQtDUlPy6OtnXDu4y+wyJCniy8ivG3+LAe4klzn1Q==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.635.0", + "@aws-sdk/core": "3.629.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", @@ -831,26 +869,26 @@ "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -862,14 +900,14 @@ } }, "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.635.0.tgz", - "integrity": "sha512-RIwDlhzAFttB1vbpznewnPqz7h1H/2UhQLwB38yfZBwYQOxyxVfLV5j5VoUUX3jY4i4qH9wiHc7b02qeAOZY6g==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.632.0.tgz", + "integrity": "sha512-Oh1fIWaoZluihOCb/zDEpRTi+6an82fgJz7fyRBugyLhEtDjmvpCQ3oKjzaOhoN+4EvXAm1ZS/ZgpvXBlIRTgw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.635.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", @@ -880,26 +918,26 @@ "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -910,19 +948,19 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.635.0" + "@aws-sdk/client-sts": "^3.632.0" } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.635.0.tgz", - "integrity": "sha512-Al2ytE69+cbA44qHlelqhzWwbURikfF13Zkal9utIG5Q6T2c7r8p6sePN92n8l/x1v0FhJ5VTxKak+cPTE0CZQ==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.632.0.tgz", + "integrity": "sha512-Ss5cBH09icpTvT+jtGGuQlRdwtO7RyE9BF4ZV/CEPATdd9whtJt4Qxdya8BUnkWR7h5HHTrQHqai3YVYjku41A==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.635.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/credential-provider-node": "3.635.0", + "@aws-sdk/client-sso-oidc": "3.632.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", @@ -933,26 +971,26 @@ "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.15", - "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -964,16 +1002,16 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.635.0.tgz", - "integrity": "sha512-i1x/E/sgA+liUE1XJ7rj1dhyXpAKO1UKFUcTTHXok2ARjWTvszHnSXMOsB77aPbmn0fUp1JTx2kHUAZ1LVt5Bg==", + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.629.0.tgz", + "integrity": "sha512-+/ShPU/tyIBM3oY1cnjgNA/tFyHtlWq+wXF9xEKRv19NOpYbWQ+xzNwVjGq8vR07cCRqy/sDQLWPhxjtuV/FiQ==", "dependencies": { - "@smithy/core": "^2.4.0", + "@smithy/core": "^2.3.2", "@smithy/node-config-provider": "^3.1.4", "@smithy/property-provider": "^3.1.3", "@smithy/protocol-http": "^4.1.0", "@smithy/signature-v4": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "fast-xml-parser": "4.4.1", @@ -998,16 +1036,16 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.635.0.tgz", - "integrity": "sha512-iJyRgEjOCQlBMXqtwPLIKYc7Bsc6nqjrZybdMDenPDa+kmLg7xh8LxHsu9088e+2/wtLicE34FsJJIfzu3L82g==", + "version": "3.622.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.622.0.tgz", + "integrity": "sha512-VUHbr24Oll1RK3WR8XLUugLpgK9ZuxEm/NVeVqyFts1Ck9gsKpRg1x4eH7L7tW3SJ4TDEQNMbD7/7J+eoL2svg==", "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/fetch-http-handler": "^3.2.4", "@smithy/node-http-handler": "^3.1.4", "@smithy/property-provider": "^3.1.3", "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" @@ -1017,14 +1055,14 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.635.0.tgz", - "integrity": "sha512-+OqcNhhOFFY08YHLjO9/Y1n37RKAO7LADnsJ7VTXca7IfvYh27BVBn+FdlqnyEb1MQ5ArHTY4pq3pKRIg6RW4Q==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.632.0.tgz", + "integrity": "sha512-m6epoW41xa1ajU5OiHcmQHoGVtrbXBaRBOUhlCLZmcaqMLYsboM4iD/WZP8aatKEON5tTnVXh/4StV8D/+wemw==", "dependencies": { "@aws-sdk/credential-provider-env": "3.620.1", - "@aws-sdk/credential-provider-http": "3.635.0", + "@aws-sdk/credential-provider-http": "3.622.0", "@aws-sdk/credential-provider-process": "3.620.1", - "@aws-sdk/credential-provider-sso": "3.635.0", + "@aws-sdk/credential-provider-sso": "3.632.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", @@ -1037,19 +1075,19 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.635.0" + "@aws-sdk/client-sts": "^3.632.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.635.0.tgz", - "integrity": "sha512-bmd23mnb94S6AxmWPgqJTnvT9ONKlTx7EPafE1RNO+vUl6mHih4iyqX6ZPaRcSfaPx4U1R7H1RM8cSnafXgaBg==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.632.0.tgz", + "integrity": "sha512-cL8fuJWm/xQBO4XJPkeuZzl3XinIn9EExWgzpG48NRMKR5us1RI/ucv7xFbBBaG+r/sDR2HpYBIA3lVIpm1H3Q==", "dependencies": { "@aws-sdk/credential-provider-env": "3.620.1", - "@aws-sdk/credential-provider-http": "3.635.0", - "@aws-sdk/credential-provider-ini": "3.635.0", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.632.0", "@aws-sdk/credential-provider-process": "3.620.1", - "@aws-sdk/credential-provider-sso": "3.635.0", + "@aws-sdk/credential-provider-sso": "3.632.0", "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", "@smithy/credential-provider-imds": "^3.2.0", @@ -1078,11 +1116,11 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.635.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.635.0.tgz", - "integrity": "sha512-hO/fKyvUaGpK9zyvCnmJz70EputvGWDr2UTOn/RzvcR6UB4yXoFf0QcCMubEsE3v67EsAv6PadgOeJ0vz6IazA==", + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.632.0.tgz", + "integrity": "sha512-P/4wB6j7ym5QCPTL2xlMfvf2NcXSh+z0jmsZP4WW/tVwab4hvgabPPbLeEZDSWZ0BpgtxKGvRq0GSHuGeirQbA==", "dependencies": { - "@aws-sdk/client-sso": "3.635.0", + "@aws-sdk/client-sso": "3.632.0", "@aws-sdk/token-providers": "3.614.0", "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", @@ -11289,12 +11327,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, "node_modules/google-auth-library": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.9.0.tgz", @@ -16603,26 +16635,6 @@ "node": "^12.20.0 || >=14" } }, - "node_modules/tsconfck": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", - "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", - "dev": true, - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^14.13.1 || ^16 || >=18" - }, - "peerDependencies": { - "typescript": "^4.3.5 || ^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -17779,48 +17791,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/vite-tsconfig-paths": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.2.tgz", - "integrity": "sha512-dq0FjyxHHDnp0uS3P12WEOX2W7NeuLzX9AWP38D7Zw2CTbFErapwQVlCiT5DMJcVWKQ1MMdTe92PZl/rBQ7qcw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^2.1.0" - }, - "peerDependencies": { - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vite-tsconfig-paths/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vite-tsconfig-paths/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/vite/node_modules/@esbuild/android-arm": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", diff --git a/backend/package.json b/backend/package.json index 967ccfb78..398dcbf26 100644 --- a/backend/package.json +++ b/backend/package.json @@ -103,7 +103,6 @@ "tsup": "^8.0.1", "tsx": "^4.4.0", "typescript": "^5.3.2", - "vite-tsconfig-paths": "^4.2.2", "vitest": "^1.2.2" }, "dependencies": { diff --git a/backend/src/db/migrations/20240702131735_secret-approval-groups.ts b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts index 537230b47..84824ac65 100644 --- a/backend/src/db/migrations/20240702131735_secret-approval-groups.ts +++ b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts @@ -115,7 +115,14 @@ export async function down(knex: Knex): Promise { // eslint-disable-next-line // @ts-ignore because generate schema happens after this approverId: knex(TableName.ProjectMembership) - .select("id") + .join( + TableName.SecretApprovalPolicy, + `${TableName.SecretApprovalPolicy}.id`, + `${TableName.SecretApprovalPolicyApprover}.policyId` + ) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`) + .select(knex.ref("id").withSchema(TableName.ProjectMembership)) + .where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`])) .where("userId", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverUserId`])) }); await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { @@ -147,13 +154,27 @@ export async function down(knex: Knex): Promise { // eslint-disable-next-line // @ts-ignore because generate schema happens after this committerId: knex(TableName.ProjectMembership) - .select("id") - .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])), + .join( + TableName.SecretApprovalPolicy, + `${TableName.SecretApprovalPolicy}.id`, + `${TableName.SecretApprovalRequest}.policyId` + ) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`) + .where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`])) + .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])) + .select(knex.ref("id").withSchema(TableName.ProjectMembership)), // eslint-disable-next-line // @ts-ignore because generate schema happens after this statusChangeBy: knex(TableName.ProjectMembership) - .select("id") + .join( + TableName.SecretApprovalPolicy, + `${TableName.SecretApprovalPolicy}.id`, + `${TableName.SecretApprovalRequest}.policyId` + ) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`) + .where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`])) .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangedByUserId`])) + .select(knex.ref("id").withSchema(TableName.ProjectMembership)) }); await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => { @@ -177,8 +198,20 @@ export async function down(knex: Knex): Promise { // eslint-disable-next-line // @ts-ignore because generate schema happens after this member: knex(TableName.ProjectMembership) - .select("id") + .join( + TableName.SecretApprovalRequest, + `${TableName.SecretApprovalRequest}.id`, + `${TableName.SecretApprovalRequestReviewer}.requestId` + ) + .join( + TableName.SecretApprovalPolicy, + `${TableName.SecretApprovalPolicy}.id`, + `${TableName.SecretApprovalRequest}.policyId` + ) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`) + .where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`])) .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`])) + .select(knex.ref("id").withSchema(TableName.ProjectMembership)) }); await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => { tb.uuid("member").notNullable().alter(); diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index fbf1af43b..b19faaf70 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -// TODO(akhilmhdh): Fix this when licence service gets it type +// TODO(akhilmhdh): Fix this when license service gets it type import { z } from "zod"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/license-fns.ts similarity index 89% rename from backend/src/ee/services/license/__mocks__/licence-fns.ts rename to backend/src/ee/services/license/__mocks__/license-fns.ts index a8b3b351d..360b39f28 100644 --- a/backend/src/ee/services/license/__mocks__/licence-fns.ts +++ b/backend/src/ee/services/license/__mocks__/license-fns.ts @@ -26,8 +26,10 @@ export const getDefaultOnPremFeatures = () => { status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true, caCrl: false }; }; + +export const setupLicenseRequestWithStore = () => {}; diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/license-fns.ts similarity index 84% rename from backend/src/ee/services/license/licence-fns.ts rename to backend/src/ee/services/license/license-fns.ts index beb981a5a..fa67b72d1 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -49,15 +49,15 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ pkiEst: false }); -export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { +export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { let token: string; - const licenceReq = axios.create({ + const licenseReq = axios.create({ baseURL, timeout: 35 * 1000 // signal: AbortSignal.timeout(60 * 1000) }); - const refreshLicence = async () => { + const refreshLicense = async () => { const appCfg = getConfig(); const { data: { token: authToken } @@ -75,7 +75,7 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string return token; }; - licenceReq.interceptors.request.use( + licenseReq.interceptors.request.use( (config) => { if (token && config.headers) { // eslint-disable-next-line no-param-reassign @@ -86,7 +86,7 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string (err) => Promise.reject(err) ); - licenceReq.interceptors.response.use( + licenseReq.interceptors.response.use( (response) => response, async (err) => { const originalRequest = (err as AxiosError).config; @@ -97,15 +97,15 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string (originalRequest as any)._retry = true; // injected // refresh - await refreshLicence(); + await refreshLicense(); - licenceReq.defaults.headers.common.Authorization = `Bearer ${token}`; - return licenceReq(originalRequest!); + licenseReq.defaults.headers.common.Authorization = `Bearer ${token}`; + return licenseReq(originalRequest!); } return Promise.reject(err); } ); - return { request: licenceReq, refreshLicence }; + return { request: licenseReq, refreshLicense }; }; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index f5360bbdf..28a9af445 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -16,8 +16,8 @@ import { TOrgDALFactory } from "@app/services/org/org-dal"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { getDefaultOnPremFeatures, setupLicenceRequestWithStore } from "./licence-fns"; import { TLicenseDALFactory } from "./license-dal"; +import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns"; import { InstanceType, TAddOrgPmtMethodDTO, @@ -64,13 +64,13 @@ export const licenseServiceFactory = ({ let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); const appCfg = getConfig(); - const licenseServerCloudApi = setupLicenceRequestWithStore( + const licenseServerCloudApi = setupLicenseRequestWithStore( appCfg.LICENSE_SERVER_URL || "", LICENSE_SERVER_CLOUD_LOGIN, appCfg.LICENSE_SERVER_KEY || "" ); - const licenseServerOnPremApi = setupLicenceRequestWithStore( + const licenseServerOnPremApi = setupLicenseRequestWithStore( appCfg.LICENSE_SERVER_URL || "", LICENSE_SERVER_ON_PREM_LOGIN, appCfg.LICENSE_KEY || "" @@ -79,7 +79,7 @@ export const licenseServiceFactory = ({ const init = async () => { try { if (appCfg.LICENSE_SERVER_KEY) { - const token = await licenseServerCloudApi.refreshLicence(); + const token = await licenseServerCloudApi.refreshLicense(); if (token) instanceType = InstanceType.Cloud; logger.info(`Instance type: ${InstanceType.Cloud}`); isValidLicense = true; @@ -87,7 +87,7 @@ export const licenseServiceFactory = ({ } if (appCfg.LICENSE_KEY) { - const token = await licenseServerOnPremApi.refreshLicence(); + const token = await licenseServerOnPremApi.refreshLicense(); if (token) { const { data: { currentPlan } diff --git a/backend/src/services/integration-auth/integration-delete-secret.ts b/backend/src/services/integration-auth/integration-delete-secret.ts index 5da48fb6e..bd9619ad4 100644 --- a/backend/src/services/integration-auth/integration-delete-secret.ts +++ b/backend/src/services/integration-auth/integration-delete-secret.ts @@ -30,6 +30,7 @@ const getIntegrationSecretsV2 = async ( environment: string; folderId: string; depth: number; + secretPath: string; decryptor: (value: Buffer | null | undefined) => string; }, secretV2BridgeDAL: Pick, @@ -306,6 +307,7 @@ export const deleteIntegrationSecrets = async ({ ? await getIntegrationSecretsV2( { environment: integration.environment.id, + secretPath: integration.secretPath, projectId: integration.projectId, folderId: folder.id, depth: 1, diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index f7c31ac61..981a1bf62 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -158,9 +158,12 @@ export const fnSecretsV2FromImports = async ({ depth?: number; cyclicDetector?: Set; decryptor: (value?: Buffer | null) => string; - expandSecretReferences?: ( - secrets: Record - ) => Promise>; + expandSecretReferences?: (inputSecret: { + value?: string; + skipMultilineEncoding?: boolean | null; + secretPath: string; + environment: string; + }) => Promise; }) => { // avoid going more than a depth if (depth >= LEVEL_BREAK) return []; @@ -244,26 +247,21 @@ export const fnSecretsV2FromImports = async ({ }); if (expandSecretReferences) { - await Promise.all( - processedImports.map(async (processedImport) => { - const secretsGroupByKey = processedImport.secrets.reduce( - (acc, item) => { - acc[item.secretKey] = { - value: item.secretValue, - comment: item.secretComment, - skipMultilineEncoding: item.skipMultilineEncoding - }; - return acc; - }, - {} as Record - ); - // eslint-disable-next-line - await expandSecretReferences(secretsGroupByKey); - processedImport.secrets.forEach((decryptedSecret) => { - // eslint-disable-next-line no-param-reassign - decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value; - }); - }) + await Promise.allSettled( + processedImports.map((processedImport) => + Promise.allSettled( + processedImport.secrets.map(async (decryptedSecret, index) => { + const expandedSecretValue = await expandSecretReferences({ + value: decryptedSecret.secretValue, + secretPath: processedImport.secretPath, + environment: processedImport.environment, + skipMultilineEncoding: decryptedSecret.skipMultilineEncoding + }); + // eslint-disable-next-line no-param-reassign + processedImport.secrets[index].secretValue = expandedSecretValue || ""; + }) + ) + ) ); } diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index e58960a4b..7079c17ef 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -377,150 +377,116 @@ type TInterpolateSecretArg = { folderDAL: Pick; }; +const MAX_SECRET_REFERENCE_DEPTH = 10; export const expandSecretReferencesFactory = ({ projectId, decryptSecretValue: decryptSecret, secretDAL, folderDAL }: TInterpolateSecretArg) => { - const fetchSecretFactory = () => { - const secretCache: Record> = {}; + const secretCache: Record> = {}; + const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; - return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => { - const referredSecretPathURL = path.join("/", ...secRefPath); - const uniqueKey = `${secRefEnv}-${referredSecretPathURL}`; + const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => { + const cacheKey = getCacheUniqueKey(environment, secretPath); - if (secretCache?.[uniqueKey]) { - return secretCache[uniqueKey][secRefKey]; - } + if (secretCache?.[cacheKey]) { + return secretCache[cacheKey][secretKey] || ""; + } - const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, referredSecretPathURL); - if (!folder) return ""; - const secrets = await secretDAL.findByFolderId(folder.id); + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return ""; + const secrets = await secretDAL.findByFolderId(folder.id); - const decryptedSecret = secrets.reduce>((prev, secret) => { - // eslint-disable-next-line - prev[secret.key] = decryptSecret(secret.encryptedValue) || ""; - return prev; - }, {}); + const decryptedSecret = secrets.reduce>((prev, secret) => { + // eslint-disable-next-line no-param-reassign + prev[secret.key] = decryptSecret(secret.encryptedValue) || ""; + return prev; + }, {}); - secretCache[uniqueKey] = decryptedSecret; + secretCache[cacheKey] = decryptedSecret; - return secretCache[uniqueKey][secRefKey]; - }; + return secretCache[cacheKey][secretKey] || ""; }; - const recursivelyExpandSecret = async ( - expandedSec: Record, - interpolatedSec: Record, - fetchSecret: (env: string, secPath: string[], secKey: string) => Promise, - recursionChainBreaker: Record, - key: string - ): Promise => { - if (expandedSec?.[key] !== undefined) { - return expandedSec[key]; - } - if (recursionChainBreaker?.[key]) { - return ""; - } - // eslint-disable-next-line - recursionChainBreaker[key] = true; + const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => { + if (!dto.value) return ""; - let interpolatedValue = interpolatedSec[key]; - if (!interpolatedValue) { - // eslint-disable-next-line no-console - console.error(`Couldn't find referenced value - ${key}`); - return ""; - } + const stack = [{ ...dto, depth: 0 }]; + let expandedValue = dto.value; - const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG); - if (refs) { - for (const interpolationSyntax of refs) { - const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); - const entities = interpolationKey.trim().split("."); + while (stack.length) { + const { value, secretPath, environment, depth } = stack.pop()!; + // eslint-disable-next-line no-continue + if (depth > MAX_SECRET_REFERENCE_DEPTH) continue; + const refs = value?.match(INTERPOLATION_SYNTAX_REG); - if (entities.length === 1) { - // eslint-disable-next-line - const val = await recursivelyExpandSecret( - expandedSec, - interpolatedSec, - fetchSecret, - recursionChainBreaker, - interpolationKey - ); - if (val) { - interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); - } - // eslint-disable-next-line - continue; - } + if (refs) { + for (const interpolationSyntax of refs) { + const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); + const entities = interpolationKey.trim().split("."); - if (entities.length > 1) { - const secRefEnv = entities[0]; - const secRefPath = entities.slice(1, entities.length - 1); - const secRefKey = entities[entities.length - 1]; + // eslint-disable-next-line no-continue + if (!entities.length) continue; - // eslint-disable-next-line - const val = await fetchSecret(secRefEnv, secRefPath, secRefKey); - if (val) { - interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); + if (entities.length === 1) { + const [secretKey] = entities; + + // eslint-disable-next-line no-continue,no-await-in-loop + const referedValue = await fetchSecret(environment, secretPath, secretKey); + const cacheKey = getCacheUniqueKey(environment, secretPath); + secretCache[cacheKey][secretKey] = referedValue; + if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { + stack.push({ + value: referedValue, + secretPath, + environment, + depth: depth + 1 + }); + } + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + } else { + const secretReferenceEnvironment = entities[0]; + const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); + const secretReferenceKey = entities[entities.length - 1]; + + // eslint-disable-next-line no-await-in-loop + const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); + const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); + secretCache[cacheKey][secretReferenceKey] = referedValue; + if (INTERPOLATION_SYNTAX_REG.test(referedValue)) { + stack.push({ + value: referedValue, + secretPath: secretReferencePath, + environment: secretReferenceEnvironment, + depth: depth + 1 + }); + } + + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); } } } } - // eslint-disable-next-line - expandedSec[key] = interpolatedValue; - return interpolatedValue; + return expandedValue; }; - const fetchSecret = fetchSecretFactory(); - const expandSecrets = async ( - inputSecrets: Record - ) => { - const expandedSecrets: Record = {}; - const toBeExpandedSecrets: Record = {}; + const expandSecret = async (inputSecret: { + value?: string; + skipMultilineEncoding?: boolean | null; + secretPath: string; + environment: string; + }) => { + if (!inputSecret.value) return inputSecret.value; - Object.keys(inputSecrets).forEach((key) => { - if (inputSecrets[key].value?.match(INTERPOLATION_SYNTAX_REG)) { - toBeExpandedSecrets[key] = inputSecrets[key].value; - } else { - expandedSecrets[key] = inputSecrets[key].value; - } - }); + const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG)); + if (!shouldExpand) return inputSecret.value; - for (const key of Object.keys(inputSecrets)) { - if (expandedSecrets?.[key]) { - // should not do multi line encoding if user has set it to skip - // eslint-disable-next-line - inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding - ? formatMultiValueEnv(expandedSecrets[key]) - : expandedSecrets[key]; - // eslint-disable-next-line - continue; - } - - // this is to avoid recursion loop. So the graph should be direct graph rather than cyclic - // so for any recursion building if there is an entity two times same key meaning it will be looped - const recursionChainBreaker: Record = {}; - // eslint-disable-next-line - const expandedVal = await recursivelyExpandSecret( - expandedSecrets, - toBeExpandedSecrets, - fetchSecret, - recursionChainBreaker, - key - ); - - // eslint-disable-next-line - inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding - ? formatMultiValueEnv(expandedVal) - : expandedVal; - } - - return inputSecrets; + const expandedSecretValue = await recursivelyExpandSecret(inputSecret); + return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue; }; - return expandSecrets; + return expandSecret; }; export const reshapeBridgeSecret = ( diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index b594f98ca..ed9cd3a7d 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -521,27 +521,22 @@ export const secretV2BridgeServiceFactory = ({ if (shouldExpandSecretReferences) { const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath); - for (const secretPathKey in secretsGroupByPath) { - if (Object.hasOwn(secretsGroupByPath, secretPathKey)) { - const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce( - (acc, item) => { - acc[item.secretKey] = { - value: item.secretValue, - comment: item.secretComment, - skipMultilineEncoding: item.skipMultilineEncoding - }; - return acc; - }, - {} as Record - ); - // eslint-disable-next-line - await expandSecretReferences(secretsGroupByKey); - secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => { - // eslint-disable-next-line no-param-reassign - decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || ""; - }); - } - } + await Promise.allSettled( + Object.keys(secretsGroupByPath).map((groupedPath) => + Promise.allSettled( + secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { + const expandedSecretValue = await expandSecretReferences({ + value: decryptedSecret.secretValue, + secretPath: groupedPath, + environment, + skipMultilineEncoding: decryptedSecret.skipMultilineEncoding + }); + // eslint-disable-next-line no-param-reassign + secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || ""; + }) + ) + ) + ); } if (!includeImports) { @@ -693,12 +688,14 @@ export const secretV2BridgeServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() : ""; if (shouldExpandSecretReferences && secretValue) { - const secretReferenceExpandedRecord = { - [secret.key]: { value: secretValue } - }; // eslint-disable-next-line - await expandSecretReferences(secretReferenceExpandedRecord); - secretValue = secretReferenceExpandedRecord[secret.key].value; + const expandedSecretValue = await expandSecretReferences({ + environment, + secretPath: path, + value: secretValue, + skipMultilineEncoding: secret.skipMultilineEncoding + }); + secretValue = expandedSecretValue || ""; } return reshapeBridgeSecret(projectId, environment, path, { diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index d41770846..7837b716b 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -196,6 +196,13 @@ export const recursivelyGetSecretPaths = ({ return getPaths; }; +// used to convert multi line ones to quotes ones with \n +const formatMultiValueEnv = (val?: string) => { + if (!val) return ""; + if (!val.match("\n")) return val; + return `"${val.replace(/\n/g, "\\n")}"`; +}; + type TInterpolateSecretArg = { projectId: string; secretEncKey: string; @@ -203,162 +210,128 @@ type TInterpolateSecretArg = { folderDAL: Pick; }; +const MAX_SECRET_REFERENCE_DEPTH = 5; const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g; export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => { - const fetchSecretsCrossEnv = () => { - const fetchCache: Record> = {}; + const secretCache: Record> = {}; + const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`; - return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => { - const secRefPathUrl = path.join("/", ...secRefPath); - const uniqKey = `${secRefEnv}-${secRefPathUrl}`; + const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => { + const cacheKey = getCacheUniqueKey(environment, secretPath); + const uniqKey = `${environment}-${cacheKey}`; - if (fetchCache?.[uniqKey]) { - return fetchCache[uniqKey][secRefKey]; - } + if (secretCache?.[uniqKey]) { + return secretCache[uniqKey][secretKey] || ""; + } - const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, secRefPathUrl); - if (!folder) return ""; - const secrets = await secretDAL.findByFolderId(folder.id); + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return ""; + const secrets = await secretDAL.findByFolderId(folder.id); - const decryptedSec = secrets.reduce>((prev, secret) => { - const secretKey = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key: secretEncKey - }); - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key: secretEncKey - }); + const decryptedSec = secrets.reduce>((prev, secret) => { + const decryptedSecretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: secretEncKey + }); + const decryptedSecretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: secretEncKey + }); - // eslint-disable-next-line - prev[secretKey] = secretValue; - return prev; - }, {}); + // eslint-disable-next-line + prev[decryptedSecretKey] = decryptedSecretValue; + return prev; + }, {}); - fetchCache[uniqKey] = decryptedSec; + secretCache[uniqKey] = decryptedSec; - return fetchCache[uniqKey][secRefKey]; - }; + return secretCache[uniqKey][secretKey] || ""; }; - const recursivelyExpandSecret = async ( - expandedSec: Record, - interpolatedSec: Record, - fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise, - recursionChainBreaker: Record, - key: string - ) => { - if (expandedSec?.[key] !== undefined) { - return expandedSec[key]; - } - if (recursionChainBreaker?.[key]) { - return ""; - } - // eslint-disable-next-line - recursionChainBreaker[key] = true; + const recursivelyExpandSecret = async ({ + value, + secretPath, + environment, + depth = 0 + }: { + value?: string; + secretPath: string; + environment: string; + depth?: number; + }) => { + if (!value) return ""; + if (depth > MAX_SECRET_REFERENCE_DEPTH) return ""; - let interpolatedValue = interpolatedSec[key]; - if (!interpolatedValue) { - // eslint-disable-next-line no-console - console.error(`Couldn't find referenced value - ${key}`); - return ""; - } - - const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG); + const refs = value.match(INTERPOLATION_SYNTAX_REG); + let expandedValue = value; if (refs) { for (const interpolationSyntax of refs) { const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1); const entities = interpolationKey.trim().split("."); if (entities.length === 1) { - const val = await recursivelyExpandSecret( - expandedSec, - interpolatedSec, - fetchCrossEnv, - recursionChainBreaker, - interpolationKey - ); - if (val) { - interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); - } + const [secretKey] = entities; // eslint-disable-next-line - continue; + let referenceValue = await fetchSecret(environment, secretPath, secretKey); + if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) { + // eslint-disable-next-line + referenceValue = await recursivelyExpandSecret({ + environment, + secretPath, + value: referenceValue, + depth: depth + 1 + }); + } + const cacheKey = getCacheUniqueKey(environment, secretPath); + secretCache[cacheKey][secretKey] = referenceValue; + expandedValue = expandedValue.replaceAll(interpolationSyntax, referenceValue); } if (entities.length > 1) { - const secRefEnv = entities[0]; - const secRefPath = entities.slice(1, entities.length - 1); - const secRefKey = entities[entities.length - 1]; + const secretReferenceEnvironment = entities[0]; + const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); + const secretReferenceKey = entities[entities.length - 1]; - const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey); - if (val) { - interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val); + // eslint-disable-next-line + let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); + if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) { + // eslint-disable-next-line + referenceValue = await recursivelyExpandSecret({ + environment: secretReferenceEnvironment, + secretPath: secretReferencePath, + value: referenceValue, + depth: depth + 1 + }); } + const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); + secretCache[cacheKey][secretReferenceKey] = referenceValue; + expandedValue = expandedValue.replaceAll(interpolationSyntax, referenceValue); } } } - // eslint-disable-next-line - expandedSec[key] = interpolatedValue; - return interpolatedValue; + return expandedValue; }; - // used to convert multi line ones to quotes ones with \n - const formatMultiValueEnv = (val?: string) => { - if (!val) return ""; - if (!val.match("\n")) return val; - return `"${val.replace(/\n/g, "\\n")}"`; + const expandSecret = async (inputSecret: { + value?: string; + skipMultilineEncoding?: boolean | null; + secretPath: string; + environment: string; + }) => { + if (!inputSecret.value) return inputSecret.value; + + const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG)); + if (!shouldExpand) return inputSecret.value; + + const expandedSecretValue = await recursivelyExpandSecret(inputSecret); + return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue; }; - - const expandSecrets = async ( - secrets: Record - ) => { - const expandedSec: Record = {}; - const interpolatedSec: Record = {}; - - const crossSecEnvFetch = fetchSecretsCrossEnv(); - - Object.keys(secrets).forEach((key) => { - if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) { - interpolatedSec[key] = secrets[key].value; - } else { - expandedSec[key] = secrets[key].value; - } - }); - - for (const key of Object.keys(secrets)) { - if (expandedSec?.[key]) { - // should not do multi line encoding if user has set it to skip - // eslint-disable-next-line - secrets[key].value = secrets[key].skipMultilineEncoding - ? formatMultiValueEnv(expandedSec[key]) - : expandedSec[key]; - // eslint-disable-next-line - continue; - } - - // this is to avoid recursion loop. So the graph should be direct graph rather than cyclic - // so for any recursion building if there is an entity two times same key meaning it will be looped - const recursionChainBreaker: Record = {}; - const expandedVal = await recursivelyExpandSecret( - expandedSec, - interpolatedSec, - crossSecEnvFetch, - recursionChainBreaker, - key - ); - - // eslint-disable-next-line - secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal; - } - - return secrets; - }; - return expandSecrets; + return expandSecret; }; export const decryptSecretRaw = ( diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 36edcf195..0c63eb147 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -258,6 +258,7 @@ export const secretQueueFactory = ({ const getIntegrationSecretsV2 = async (dto: { projectId: string; environment: string; + secretPath: string; folderId: string; depth: number; decryptor: (value: Buffer | null | undefined) => string; @@ -269,30 +270,36 @@ export const secretQueueFactory = ({ ); return content; } - - // process secrets in current folder - const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId); - secrets.forEach((secret) => { - const secretKey = secret.key; - const secretValue = dto.decryptor(secret.encryptedValue); - content[secretKey] = { value: secretValue }; - - if (secret.encryptedComment) { - const commentValue = dto.decryptor(secret.encryptedComment); - content[secretKey].comment = commentValue; - } - - content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); - }); - const expandSecretReferences = expandSecretReferencesFactory({ decryptSecretValue: dto.decryptor, secretDAL: secretV2BridgeDAL, folderDAL, projectId: dto.projectId }); + // process secrets in current folder + const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId); + + await Promise.allSettled( + secrets.map(async (secret) => { + const secretKey = secret.key; + const secretValue = dto.decryptor(secret.encryptedValue); + const expandedSecretValue = await expandSecretReferences({ + environment: dto.environment, + secretPath: dto.secretPath, + skipMultilineEncoding: secret.skipMultilineEncoding, + value: secretValue + }); + content[secretKey] = { value: expandedSecretValue || "" }; + + if (secret.encryptedComment) { + const commentValue = dto.decryptor(secret.encryptedComment); + content[secretKey].comment = commentValue; + } + + content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + }) + ); - await expandSecretReferences(content); // check if current folder has any imports from other folders const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false }); @@ -329,6 +336,7 @@ export const secretQueueFactory = ({ const getIntegrationSecrets = async (dto: { projectId: string; environment: string; + secretPath: string; folderId: string; key: string; depth: number; @@ -341,46 +349,52 @@ export const secretQueueFactory = ({ return content; } - // process secrets in current folder - const secrets = await secretDAL.findByFolderId(dto.folderId); - secrets.forEach((secret) => { - const secretKey = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key: dto.key - }); - - const secretValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key: dto.key - }); - - content[secretKey] = { value: secretValue }; - - if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { - const commentValue = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secret.secretCommentCiphertext, - iv: secret.secretCommentIV, - tag: secret.secretCommentTag, - key: dto.key - }); - content[secretKey].comment = commentValue; - } - - content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); - }); - - const expandSecrets = interpolateSecrets({ + const expandSecretReferences = interpolateSecrets({ projectId: dto.projectId, secretEncKey: dto.key, folderDAL, secretDAL }); - await expandSecrets(content); + // process secrets in current folder + const secrets = await secretDAL.findByFolderId(dto.folderId); + await Promise.allSettled( + secrets.map(async (secret) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: dto.key + }); + + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: dto.key + }); + const expandedSecretValue = await expandSecretReferences({ + environment: dto.environment, + secretPath: dto.secretPath, + skipMultilineEncoding: secret.skipMultilineEncoding, + value: secretValue + }); + + content[secretKey] = { value: expandedSecretValue || "" }; + + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + const commentValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key: dto.key + }); + content[secretKey].comment = commentValue; + } + + content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + }) + ); // check if current folder has any imports from other folders const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false }); @@ -404,7 +418,8 @@ export const secretQueueFactory = ({ projectId: dto.projectId, folderId: folder.id, key: dto.key, - depth: dto.depth + 1 + depth: dto.depth + 1, + secretPath: dto.secretPath }); // add the imported secrets to the current folder secrets @@ -686,6 +701,7 @@ export const secretQueueFactory = ({ projectId, folderId: folder.id, depth: 1, + secretPath, decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "") }) : await getIntegrationSecrets({ @@ -693,7 +709,8 @@ export const secretQueueFactory = ({ projectId, folderId: folder.id, key: botKey as string, - depth: 1 + depth: 1, + secretPath }); for (const integration of toBeSyncedIntegrations) { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index c62a73f69..e502f577a 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -482,7 +482,7 @@ export const secretServiceFactory = ({ projectId, environmentSlug: folder.environment.slug }); - // TODO(akhilmhdh-pg): licence check, posthog service and snapshot + // TODO(akhilmhdh-pg): license check, posthog service and snapshot return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; }; @@ -1047,74 +1047,47 @@ export const secretServiceFactory = ({ }; }); + const expandSecret = interpolateSecrets({ + folderDAL, + projectId, + secretDAL, + secretEncKey: botKey + }); + if (expandSecretReferences) { - const expandSecrets = interpolateSecrets({ - folderDAL, - projectId, - secretDAL, - secretEncKey: botKey - }); - - const batchSecretsExpand = async ( - secretBatch: { - secretKey: string; - secretValue: string; - secretComment?: string; - secretPath: string; - skipMultilineEncoding: boolean | null | undefined; - }[] - ) => { - // Group secrets by secretPath - const secretsByPath: Record< - string, - { - secretKey: string; - secretValue: string; - secretComment?: string; - skipMultilineEncoding: boolean | null | undefined; - }[] - > = {}; - - secretBatch.forEach((secret) => { - if (!secretsByPath[secret.secretPath]) { - secretsByPath[secret.secretPath] = []; - } - secretsByPath[secret.secretPath].push(secret); - }); - - // Expand secrets for each group - for (const secPath in secretsByPath) { - if (!Object.hasOwn(secretsByPath, path)) { - // eslint-disable-next-line no-continue - continue; - } - - const secretRecord: Record< - string, - { value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined } - > = {}; - secretsByPath[secPath].forEach((decryptedSecret) => { - secretRecord[decryptedSecret.secretKey] = { - value: decryptedSecret.secretValue, - comment: decryptedSecret.secretComment, - skipMultilineEncoding: decryptedSecret.skipMultilineEncoding - }; - }); - - await expandSecrets(secretRecord); - - secretsByPath[secPath].forEach((decryptedSecret) => { - // eslint-disable-next-line no-param-reassign - decryptedSecret.secretValue = secretRecord[decryptedSecret.secretKey].value; - }); - } - }; - - // expand secrets - await batchSecretsExpand(filteredSecrets); - - // expand imports by batch - await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets))); + const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath); + await Promise.allSettled( + Object.keys(secretsGroupByPath).map((groupedPath) => + Promise.allSettled( + secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { + const expandedSecretValue = await expandSecret({ + value: decryptedSecret.secretValue, + secretPath: groupedPath, + environment, + skipMultilineEncoding: decryptedSecret.skipMultilineEncoding + }); + // eslint-disable-next-line no-param-reassign + secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || ""; + }) + ) + ) + ); + await Promise.allSettled( + processedImports.map((processedImport) => + Promise.allSettled( + processedImport.secrets.map(async (decryptedSecret, index) => { + const expandedSecretValue = await expandSecret({ + value: decryptedSecret.secretValue, + secretPath: path, + environment, + skipMultilineEncoding: decryptedSecret.skipMultilineEncoding + }); + // eslint-disable-next-line no-param-reassign + processedImport.secrets[index].secretValue = expandedSecretValue || ""; + }) + ) + ) + ); } return { @@ -1177,40 +1150,19 @@ export const secretServiceFactory = ({ const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); if (expandSecretReferences) { - const expandSecrets = interpolateSecrets({ + const expandSecret = interpolateSecrets({ folderDAL, projectId, secretDAL, secretEncKey: botKey }); - - const expandSingleSecret = async (secret: { - secretKey: string; - secretValue: string; - secretComment?: string; - secretPath: string; - skipMultilineEncoding: boolean | null | undefined; - }) => { - const secretRecord: Record< - string, - { value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined } - > = { - [secret.secretKey]: { - value: secret.secretValue, - comment: secret.secretComment, - skipMultilineEncoding: secret.skipMultilineEncoding - } - }; - - await expandSecrets(secretRecord); - - // Update the secret with the expanded value - // eslint-disable-next-line no-param-reassign - secret.secretValue = secretRecord[secret.secretKey].value; - }; - - // Expand the secret - await expandSingleSecret(decryptedSecret); + const expandedSecretValue = await expandSecret({ + environment, + secretPath: path, + value: decryptedSecret.secretValue, + skipMultilineEncoding: decryptedSecret.skipMultilineEncoding + }); + decryptedSecret.secretValue = expandedSecretValue || ""; } return decryptedSecret; diff --git a/backend/vitest.e2e.config.ts b/backend/vitest.e2e.config.ts index c660fed14..684a4dc42 100644 --- a/backend/vitest.e2e.config.ts +++ b/backend/vitest.e2e.config.ts @@ -1,4 +1,4 @@ -import tsconfigPaths from "vite-tsconfig-paths"; // only if you are using custom tsconfig paths +import path from "path"; import { defineConfig } from "vitest/config"; export default defineConfig({ @@ -15,7 +15,14 @@ export default defineConfig({ useAtomics: true, isolate: false } + }, + alias: { + "./license-fns": path.resolve(__dirname, "./src/ee/services/license/__mocks__/license-fns") } }, - plugins: [tsconfigPaths()] // only if you are using custom tsconfig paths, + resolve: { + alias: { + "@app": path.resolve(__dirname, "./src") + } + } });