Merge remote-tracking branch 'origin/main' into feat/native-slack-integration

This commit is contained in:
Sheen Capadngan
2024-09-04 23:10:15 +08:00
93 changed files with 6556 additions and 1663 deletions
@@ -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");
});
});
+666 -64
View File
@@ -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"
});
});
}
);
@@ -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 }
);
@@ -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 }
);
});
@@ -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,
+73
View File
@@ -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[];
};
@@ -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[];
};
+128
View File
@@ -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[];
}[];
};
};
+22 -4
View File
@@ -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();
}
};
+259 -177
View File
@@ -15,6 +15,7 @@
"@aws-sdk/client-secrets-manager": "^3.504.0",
"@aws-sdk/client-sts": "^3.600.0",
"@casl/ability": "^6.5.0",
"@elastic/elasticsearch": "^8.15.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.5.0",
"@fastify/etag": "^5.1.0",
@@ -81,6 +82,8 @@
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",
@@ -136,7 +139,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 +303,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 +323,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 +332,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 +563,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 +700,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 +720,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 +752,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 +772,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 +803,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 +823,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 +855,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 +872,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 +903,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 +921,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 +951,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 +974,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 +1005,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 +1039,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 +1058,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 +1078,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 +1119,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",
@@ -3725,6 +3766,60 @@
"node": ">=12"
}
},
"node_modules/@elastic/elasticsearch": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz",
"integrity": "sha512-mG90EMdTDoT6GFSdqpUAhWK9LGuiJo6tOWqs0Usd/t15mPQDj7ZqHXfCBqNkASZpwPZpbAYVjd57S6nbUBINCg==",
"license": "Apache-2.0",
"dependencies": {
"@elastic/transport": "^8.7.0",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@elastic/transport": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.7.1.tgz",
"integrity": "sha512-2eeMVkz57Ayxv+UAZkIKzzrUu7nm96jr3+N3kLfbBqALYe2jwDpLr9pR0jc/x9HyJKAM909YGaNlHFDZeb0+Mw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "1.x",
"debug": "^4.3.4",
"hpagent": "^1.0.0",
"ms": "^2.1.3",
"secure-json-parse": "^2.4.0",
"tslib": "^2.4.0",
"undici": "^6.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@elastic/transport/node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@elastic/transport/node_modules/debug/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==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -5221,6 +5316,15 @@
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@peculiar/asn1-cms": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.3.8.tgz",
@@ -11371,12 +11475,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",
@@ -11668,6 +11766,15 @@
"node": ">= 6"
}
},
"node_modules/hpagent": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
"integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -13129,12 +13236,12 @@
}
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -15643,6 +15750,34 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
},
"node_modules/scim-patch": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz",
"integrity": "sha512-3d0wD4THAt03Zgi08kCQwc+lvPJ2v4wwk41b0xViVa4gLYSgRUCmGkJNBsaE+yoKg0fufTOJCcSrufZaqYn/og==",
"dependencies": {
"@types/node": "^22.0.0",
"fast-deep-equal": "3.1.3",
"scim2-parse-filter": "0.2.10"
}
},
"node_modules/scim-patch/node_modules/@types/node": {
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz",
"integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/scim-patch/node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/scim2-parse-filter": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
"integrity": "sha512-k5TgGSuQEbR4jXRgw/GPAYVL9fMp1pWA2abLF5z3q9IGWSuZTqbrZBOSUezvc+rtViXr+czSZjg3eAN4QSTvxQ=="
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
@@ -16721,26 +16856,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",
@@ -17560,6 +17675,15 @@
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true
},
"node_modules/undici": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -17897,48 +18021,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",
+4 -1
View File
@@ -50,6 +50,7 @@
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
@@ -102,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": {
@@ -112,6 +112,7 @@
"@aws-sdk/client-secrets-manager": "^3.504.0",
"@aws-sdk/client-sts": "^3.600.0",
"@casl/ability": "^6.5.0",
"@elastic/elasticsearch": "^8.15.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.5.0",
"@fastify/etag": "^5.1.0",
@@ -178,6 +179,8 @@
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",
@@ -115,7 +115,14 @@ export async function down(knex: Knex): Promise<void> {
// 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<void> {
// 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<void> {
// 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();
@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password");
if (!doesPasswordExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.string("password").nullable();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password");
if (doesPasswordExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("password");
});
}
}
}
+1
View File
@@ -21,6 +21,7 @@ export const SecretSharingSchema = z.object({
expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"),
name: z.string().nullable().optional(),
password: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional()
});
+1 -1
View File
@@ -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";
+185 -236
View File
@@ -5,22 +5,47 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerScimRouter = async (server: FastifyZodProvider) => {
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
const ScimUserSchema = z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
emails: z
.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
displayName: z.string().trim(),
active: z.boolean()
});
const ScimGroupSchema = z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z
.array(
z.object({
value: z.string(),
display: z.string().optional()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
});
export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/scim-tokens",
method: "POST",
@@ -127,25 +152,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
Resources: z.array(
z.object({
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
),
Resources: z.array(ScimUserSchema),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
@@ -173,30 +180,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgMembershipId: z.string().trim()
}),
response: {
201: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -216,10 +200,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
body: z.object({
schemas: z.array(z.string()),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
emails: z
.array(
z.object({
@@ -229,28 +215,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
})
)
.optional(),
// displayName: z.string().trim(),
active: z.boolean()
active: z.boolean().default(true)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -260,8 +228,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const user = await req.server.services.scim.createScimUser({
externalId: req.body.userName,
email: primaryEmail,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
firstName: req.body?.name?.givenName,
lastName: req.body?.name?.familyName,
orgId: req.permission.orgId
});
@@ -291,6 +259,116 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
displayName: z.string().trim(),
emails: z
.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
firstName: req.body?.name?.givenName,
lastName: req.body?.name?.familyName,
active: req.body?.active,
email: primaryEmail,
externalId: req.body.userName
});
return user;
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PATCH",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
Operations: z.array(
z.union([
z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim(),
value: z
.object({
value: z.string()
})
.array()
.optional()
}),
z.object({
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim().optional(),
value: z.any().optional()
})
])
)
}),
response: {
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.updateScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return user;
}
});
server.route({
url: "/Groups",
method: "POST",
@@ -305,25 +383,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
display: z.string()
})
)
.optional() // okta-specific
.optional()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -344,26 +407,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20),
filter: z.string().trim().optional()
filter: z.string().trim().optional(),
excludedAttributes: z.string().trim().optional()
}),
response: {
200: z.object({
Resources: z.array(
z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
),
Resources: z.array(ScimGroupSchema),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
@@ -377,7 +426,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId,
startIndex: req.query.startIndex,
filter: req.query.filter,
limit: req.query.count
limit: req.query.count,
isMembersExcluded: req.query.excludedAttributes === "members"
});
return groups;
@@ -392,20 +442,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: z.string().trim()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -414,6 +451,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: req.params.groupId,
orgId: req.permission.orgId
});
return group;
}
});
@@ -437,25 +475,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePut({
const group = await req.server.services.scim.replaceScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId,
...req.body
@@ -476,55 +501,35 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schemas: z.array(z.string()),
Operations: z.array(
z.union([
z.object({
op: z.union([z.literal("replace"), z.literal("Replace")]),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim()
path: z.string().trim(),
value: z
.object({
value: z.string()
})
.array()
.optional()
}),
z.object({
op: z.union([z.literal("add"), z.literal("Add")]),
path: z.string().trim(),
value: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim().optional()
})
)
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim().optional(),
value: z.any()
})
])
)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePatch({
const group = await req.server.services.scim.updateScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return group;
}
});
@@ -550,60 +555,4 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
return group;
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
displayName: z.string().trim(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
active: req.body.active
});
return user;
}
});
};
@@ -5,6 +5,7 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@@ -62,7 +63,9 @@ export const auditLogDALFactory = (db: TDbClient) => {
const today = new Date();
let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
do {
try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
@@ -84,7 +87,9 @@ export const auditLogDALFactory = (db: TDbClient) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
};
return { ...auditLogOrm, pruneAuditLog, find };
@@ -0,0 +1,126 @@
import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
) {
throw new BadRequestError({ message: "Invalid db host" });
}
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
const connection = new ElasticSearchClient({
node: {
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
...(providerInputs.ca && {
ssl: {
rejectUnauthorized: false,
ca: providerInputs.ca
}
})
},
auth: {
...(providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey
? {
apiKey: {
api_key: providerInputs.auth.apiKey,
id: providerInputs.auth.apiKeyId
}
}
: {
username: providerInputs.auth.username,
password: providerInputs.auth.password
})
}
});
return connection;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const infoResponse = await connection
.info()
.then(() => true)
.catch(() => false);
return infoResponse;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
await connection.security.putUser({
username,
password,
full_name: "Managed by Infisical.com",
roles: providerInputs.roles
});
await connection.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
await connection.security.deleteUser({
username: entityId
});
await connection.close();
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};
@@ -1,7 +1,9 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchDatabaseProvider } from "./elastic-search";
import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { RedisDatabaseProvider } from "./redis";
import { SqlDatabaseProvider } from "./sql-database";
@@ -10,5 +12,7 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider()
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider()
});
@@ -7,6 +7,11 @@ export enum SqlProviders {
MsSQL = "mssql"
}
export enum ElasticSearchAuthTypes {
User = "user",
ApiKey = "api-key"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@@ -30,6 +35,28 @@ export const DynamicSecretAwsElastiCacheSchema = z.object({
ca: z.string().optional()
});
export const DynamicSecretElasticSearchSchema = z.object({
host: z.string().trim().min(1),
port: z.number(),
roles: z.array(z.string().trim().min(1)).min(1),
// two auth types "user, apikey"
auth: z.discriminatedUnion("type", [
z.object({
type: z.literal(ElasticSearchAuthTypes.User),
username: z.string().trim(),
password: z.string().trim()
}),
z.object({
type: z.literal(ElasticSearchAuthTypes.ApiKey),
apiKey: z.string().trim(),
apiKeyId: z.string().trim()
})
]),
ca: z.string().optional()
});
export const DynamicSecretSqlDBSchema = z.object({
client: z.nativeEnum(SqlProviders),
host: z.string().trim().toLowerCase(),
@@ -67,12 +94,51 @@ export const DynamicSecretAwsIamSchema = z.object({
policyArns: z.string().trim().optional()
});
export const DynamicSecretMongoAtlasSchema = z.object({
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
adminPrivateKey: z.string().trim().min(1).describe("Admin user private api key"),
groupId: z
.string()
.trim()
.min(1)
.describe("Unique 24-hexadecimal digit string that identifies your project. This is same as project id"),
roles: z
.object({
collectionName: z.string().optional().describe("Collection on which this role applies."),
databaseName: z.string().min(1).describe("Database to which the user is granted access privileges."),
roleName: z
.string()
.min(1)
.describe(
' Enum: "atlasAdmin" "backup" "clusterMonitor" "dbAdmin" "dbAdminAnyDatabase" "enableSharding" "read" "readAnyDatabase" "readWrite" "readWriteAnyDatabase" "<a custom role name>".Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.'
)
})
.array()
.min(1),
scopes: z
.object({
name: z
.string()
.min(1)
.describe(
"Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access."
),
type: z
.string()
.min(1)
.describe("Category of resource that this database user can access. Enum: CLUSTER, DATA_LAKE, STREAM")
})
.array()
});
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
AwsIam = "aws-iam",
Redis = "redis",
AwsElastiCache = "aws-elasticache"
AwsElastiCache = "aws-elasticache",
MongoAtlas = "mongo-db-atlas",
ElasticSearch = "elastic-search"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -80,7 +146,9 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema })
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema })
]);
export type TDynamicProviderFns = {
@@ -0,0 +1,146 @@
import axios, { AxiosError } from "axios";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const MongoAtlasProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretMongoAtlasSchema.parseAsync(inputs);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
const client = axios.create({
baseURL: "https://cloud.mongodb.com/api/atlas",
headers: {
Accept: "application/vnd.atlas.2023-02-01+json",
"Content-Type": "application/json"
}
});
const digestAuth = createDigestAuthRequestInterceptor(
client,
providerInputs.adminPublicKey,
providerInputs.adminPrivateKey
);
return digestAuth;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const isConnected = await client({
method: "GET",
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
params: { itemsPerPage: 1 }
})
.then(() => true)
.catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
await client({
method: "POST",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
data: {
roles: providerInputs.roles,
scopes: providerInputs.scopes,
deleteAfterDate: expiration,
username,
password,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = entityId;
const isExisting = await client({
method: "GET",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
}).catch((err) => {
if ((err as AxiosError).response?.status === 404) return false;
throw err;
});
if (isExisting) {
await client({
method: "DELETE",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
}
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
await client({
method: "PATCH",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
data: {
deleteAfterDate: expiration,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import handlebars from "handlebars";
import { Redis } from "ioredis";
import { customAlphabet } from "nanoid";
@@ -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 = () => {};
@@ -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 };
};
@@ -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 }
+15 -11
View File
@@ -44,19 +44,18 @@ export const buildScimUser = ({
email,
firstName,
lastName,
groups = [],
active
active,
createdAt,
updatedAt
}: {
orgMembershipId: string;
username: string;
email?: string | null;
firstName: string | null | undefined;
lastName: string | null | undefined;
groups?: {
value: string;
display: string;
}[];
active: boolean;
createdAt: Date;
updatedAt: Date;
}): TScimUser => {
const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -78,10 +77,10 @@ export const buildScimUser = ({
]
: [],
active,
groups,
meta: {
resourceType: "User",
location: null
created: createdAt,
lastModified: updatedAt
}
};
@@ -109,14 +108,18 @@ export const buildScimGroupList = ({
export const buildScimGroup = ({
groupId,
name,
members
members,
updatedAt,
createdAt
}: {
groupId: string;
name: string;
members: {
value: string;
display: string;
display?: string;
}[];
createdAt: Date;
updatedAt: Date;
}): TScimGroup => {
const scimGroup = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
@@ -125,7 +128,8 @@ export const buildScimGroup = ({
members,
meta: {
resourceType: "Group",
location: null
created: createdAt,
lastModified: updatedAt
}
};
+240 -212
View File
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -9,7 +10,6 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type";
@@ -32,14 +32,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import {
buildScimGroup,
buildScimGroupList,
buildScimUser,
buildScimUserList,
extractScimValueFromPath,
parseScimFilter
} from "./scim-fns";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
import {
TCreateScimGroupDTO,
TCreateScimTokenDTO,
@@ -64,12 +57,18 @@ type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<
TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete" | "update">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
| "createMembership"
| "findById"
| "findMembership"
| "findMembershipWithScimFilter"
| "deleteMembershipById"
| "transaction"
| "updateMembershipById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
@@ -193,7 +192,12 @@ export const scimServiceFactory = ({
};
// SCIM server endpoints
const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const listScimUsers = async ({
startIndex = 0,
limit = 100,
filter,
orgId
}: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
@@ -207,23 +211,20 @@ export const scimServiceFactory = ({
...(limit && { limit })
};
const users = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseScimFilter(filter)
},
findOpts
);
const users = await orgDAL.findMembershipWithScimFilter(orgId, filter, findOpts);
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive
})
const scimUsers = users.map(
({ id, externalId, username, firstName, lastName, email, isActive, createdAt, updatedAt }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive,
createdAt,
updatedAt
})
);
return buildScimUserList({
@@ -258,11 +259,6 @@ export const scimServiceFactory = ({
status: 403
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
@@ -270,10 +266,8 @@ export const scimServiceFactory = ({
firstName: membership.firstName,
lastName: membership.lastName,
active: membership.isActive,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@@ -322,7 +316,7 @@ export const scimServiceFactory = ({
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role: OrgMembershipRole.NoAccess,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@@ -349,7 +343,11 @@ export const scimServiceFactory = ({
}
if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
const uniqueUsername = await normalizeUsername(
// external id is username
`${firstName}-${lastName}`,
userDAL
);
user = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
@@ -430,10 +428,13 @@ export const scimServiceFactory = ({
firstName: createdUser.firstName,
lastName: createdUser.lastName,
email: createdUser.email ?? "",
active: createdOrgMembership.isActive
active: createdOrgMembership.isActive,
createdAt: createdOrgMembership.createdAt,
updatedAt: createdOrgMembership.updatedAt
});
};
// partial
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
@@ -459,37 +460,52 @@ export const scimServiceFactory = ({
status: 403
});
let active = true;
operations.forEach((operation) => {
if (operation.op.toLowerCase() === "replace") {
if (operation.path === "active" && operation.value === "False") {
// azure scim op format
active = false;
} else if (typeof operation.value === "object" && operation.value.active === false) {
// okta scim op format
active = false;
}
}
});
if (!active) {
await orgMembershipDAL.updateById(membership.id, {
isActive: false
});
}
return buildScimUser({
const scimUser = buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName,
lastName: membership.lastName,
active
firstName: membership.firstName,
active: membership.isActive,
username: membership.externalId ?? membership.username,
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
scimPatch(scimUser, operations);
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await orgMembershipDAL.updateById(
membership.id,
{
isActive: scimUser.active
},
tx
);
const hasEmailChanged = scimUser.emails[0].value !== membership.email;
await userDAL.updateById(
membership.userId,
{
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true
},
tx
);
});
return scimUser;
};
const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
const replaceScimUser = async ({
orgMembershipId,
active,
orgId,
lastName,
firstName,
email,
externalId
}: TReplaceScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
@@ -514,26 +530,47 @@ export const scimServiceFactory = ({
status: 403
});
await orgMembershipDAL.updateById(membership.id, {
isActive: active
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await userAliasDAL.update(
{
orgId,
aliasType: UserAliasType.SAML,
userId: membership.userId
},
{
externalId
},
tx
);
await orgMembershipDAL.updateById(
membership.id,
{
isActive: active
},
tx
);
await userDAL.updateById(
membership.userId,
{
firstName,
email,
lastName,
isEmailVerified: serverCfg.trustSamlEmails
},
tx
);
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
username: externalId,
email: membership.email,
firstName: membership.firstName,
lastName: membership.lastName,
active,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@@ -570,7 +607,7 @@ export const scimServiceFactory = ({
return {}; // intentionally return empty object upon success
};
const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
const listScimGroups = async ({ orgId, startIndex, limit, filter, isMembersExcluded }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@@ -603,6 +640,21 @@ export const scimServiceFactory = ({
);
const scimGroups: TScimGroup[] = [];
if (isMembersExcluded) {
return buildScimGroupList({
scimGroups: groups.map((group) =>
buildScimGroup({
groupId: group.id,
name: group.name,
members: [],
createdAt: group.createdAt,
updatedAt: group.updatedAt
})
),
startIndex,
limit
});
}
for await (const group of groups) {
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
@@ -612,7 +664,9 @@ export const scimServiceFactory = ({
members: members.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
scimGroups.push(scimGroup);
}
@@ -696,7 +750,9 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
})),
createdAt: newGroup.group.createdAt,
updatedAt: newGroup.group.updatedAt
});
};
@@ -739,31 +795,17 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
};
const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const $replaceGroupDAL = async (
groupId: string,
orgId: string,
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
) => {
const updatedGroup = await groupDAL.transaction(async (tx) => {
const [group] = await groupDAL.update(
{
@@ -782,74 +824,96 @@ export const scimServiceFactory = ({
});
}
if (members) {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
const orgMemberships = members.length
? await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
})
: [];
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const userGroupMembers = await userGroupMembershipDAL.find({
groupId: group.id
});
const directMemberUserIds = userGroupMembers.filter((el) => !el.isPending).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = userGroupMembers
.filter((el) => el.isPending)
.map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const directMemberUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: false
})
).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: true
})
).map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
return group;
});
return updatedGroup;
};
const replaceScimGroup = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const updatedGroup = await $replaceGroupDAL(groupId, orgId, { displayName, members });
return buildScimGroup({
groupId: updatedGroup.id,
name: updatedGroup.name,
members
members,
updatedAt: updatedGroup.updatedAt,
createdAt: updatedGroup.createdAt
});
};
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const updateScimGroup = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@@ -871,7 +935,7 @@ export const scimServiceFactory = ({
status: 403
});
let group = await groupDAL.findOne({
const group = await groupDAL.findOne({
id: groupId,
orgId
});
@@ -883,64 +947,28 @@ export const scimServiceFactory = ({
});
}
for await (const operation of operations) {
if (operation.op === "replace" || operation.op === "Replace") {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
});
} else if (operation.op === "add" || operation.op === "Add") {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
await addUsersToGroupByUserIds({
group,
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
} else if (operation.op === "remove" || operation.op === "Remove") {
const orgMembershipId = extractScimValueFromPath(operation.path);
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
await removeUsersFromGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
} else {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
}
}
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return buildScimGroup({
const scimGroup = buildScimGroup({
groupId: group.id,
name: group.name,
members: members.map((member) => ({
value: member.orgMembershipId
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
scimPatch(scimGroup, operations);
// remove members is a weird case not following scim convention
await $replaceGroupDAL(groupId, orgId, { displayName: scimGroup.displayName, members: scimGroup.members });
const updatedScimMembers = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return {
...scimGroup,
members: updatedScimMembers.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
});
};
};
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
@@ -1016,8 +1044,8 @@ export const scimServiceFactory = ({
createScimGroup,
getScimGroup,
deleteScimGroup,
updateScimGroupNamePut,
updateScimGroupNamePatch,
replaceScimGroup,
updateScimGroup,
fnValidateScimToken
};
};
+16 -43
View File
@@ -1,3 +1,5 @@
import { ScimPatchOperation } from "scim-patch";
import { TOrgPermission } from "@app/lib/types";
export type TCreateScimTokenDTO = {
@@ -34,29 +36,25 @@ export type TGetScimUserDTO = {
export type TCreateScimUserDTO = {
externalId: string;
email?: string;
firstName: string;
lastName: string;
firstName?: string;
lastName?: string;
orgId: string;
};
export type TUpdateScimUserDTO = {
orgMembershipId: string;
orgId: string;
operations: {
op: string;
path?: string;
value?:
| string
| {
active: boolean;
};
}[];
operations: ScimPatchOperation[];
};
export type TReplaceScimUserDTO = {
orgMembershipId: string;
active: boolean;
orgId: string;
email?: string;
firstName?: string;
lastName?: string;
externalId: string;
};
export type TDeleteScimUserDTO = {
@@ -69,6 +67,7 @@ export type TListScimGroupsDTO = {
filter?: string;
limit: number;
orgId: string;
isMembersExcluded?: boolean;
};
export type TListScimGroups = {
@@ -107,31 +106,7 @@ export type TUpdateScimGroupNamePutDTO = {
export type TUpdateScimGroupNamePatchDTO = {
groupId: string;
orgId: string;
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
};
// akhilmhdh: I know, this is done due to lack of time. Need to change later to support as normalized rather than like this
// Forgive akhil blame tony
type TReplaceOp = {
op: "replace" | "Replace";
value: {
id: string;
displayName: string;
};
};
type TRemoveOp = {
op: "remove" | "Remove";
path: string;
};
type TAddOp = {
op: "add" | "Add";
path: string;
value: {
value: string;
display?: string;
}[];
operations: ScimPatchOperation[];
};
export type TDeleteScimGroupDTO = {
@@ -160,13 +135,10 @@ export type TScimUser = {
type: string;
}[];
active: boolean;
groups: {
value: string;
display: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};
@@ -176,10 +148,11 @@ export type TScimGroup = {
displayName: string;
members: {
value: string;
display: string;
display?: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};
@@ -16,6 +16,7 @@ import {
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSnapshotDALFactory = ReturnType<typeof snapshotDALFactory>;
@@ -599,6 +600,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
const pruneExcessSnapshots = async () => {
const PRUNE_FOLDER_BATCH_SIZE = 10000;
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots started`);
try {
let uuidOffset = "00000000-0000-0000-0000-000000000000";
// cleanup snapshots from current folders
@@ -714,6 +716,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
} catch (error) {
throw new DatabaseError({ error, name: "SnapshotPrune" });
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots completed`);
};
// special query for migration for secret v2
+4
View File
@@ -964,6 +964,10 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.",
githubVisibility:
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
githubVisibilityRepoIds:
"The repository IDs to sync secrets to when using the Github Integration. Only applicable when using Organization scope, and visibility is set to 'selected'",
kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
+57
View File
@@ -0,0 +1,57 @@
import crypto from "node:crypto";
import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
export const createDigestAuthRequestInterceptor = (
axiosInstance: AxiosInstance,
username: string,
password: string
) => {
let nc = 0;
return async (opts: AxiosRequestConfig) => {
try {
return await axiosInstance.request(opts);
} catch (err) {
const error = err as AxiosError;
const authHeader = (error?.response?.headers?.["www-authenticate"] as string) || "";
if (error?.response?.status !== 401 || !authHeader?.includes("nonce")) {
return Promise.reject(error.message);
}
if (!error.config) {
return Promise.reject(error);
}
const authDetails = authHeader.split(",").map((el) => el.split("="));
nc += 1;
const nonceCount = nc.toString(16).padStart(8, "0");
const cnonce = crypto.randomBytes(24).toString("hex");
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
const path = opts.url;
const ha2 = crypto
.createHash("md5")
.update(`${opts.method ?? "GET"}:${path}`)
.digest("hex");
const response = crypto
.createHash("md5")
.update(`${ha1}:${nonce}:${nonceCount}:${cnonce}:auth:${ha2}`)
.digest("hex");
const authorization = `Digest username="${username}",realm="${realm}",nonce="${nonce}",uri="${path}",qop="auth",algorithm="MD5",response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
if (opts.headers) {
// eslint-disable-next-line
opts.headers.authorization = authorization;
} else {
// eslint-disable-next-line
opts.headers = { authorization };
}
return axiosInstance.request(opts);
}
};
};
+121
View File
@@ -0,0 +1,121 @@
import { Knex } from "knex";
import { Compare, Filter, parse } from "scim2-parse-filter";
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
}
return filter;
};
export const generateKnexQueryFromScim = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
) => {
const scimRootFilterAst = parse(rootScimFilter);
const stack = [
{
scimFilterAst: scimRootFilterAst,
query: rootQuery
}
];
while (stack.length) {
const { scimFilterAst, query } = stack.pop()!;
switch (scimFilterAst.op) {
case "eq": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, scimFilterAst.compValue);
break;
}
case "pr": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNotNull(attrPath);
break;
}
case "gt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">", scimFilterAst.compValue);
break;
}
case "ge": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">=", scimFilterAst.compValue);
break;
}
case "lt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<", scimFilterAst.compValue);
break;
}
case "le": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<=", scimFilterAst.compValue);
break;
}
case "sw": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `${scimFilterAst.compValue}%`);
break;
}
case "ew": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`);
break;
}
case "co": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`);
break;
}
case "ne": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue);
break;
}
case "and": {
void query.andWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "or": {
void query.orWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "not": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: scimFilterAst.filter
});
});
break;
}
case "[]": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
});
});
break;
}
default:
break;
}
}
};
+15
View File
@@ -49,6 +49,21 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler);
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
try {
await server.register<FastifyCookieOptions>(cookie, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY
@@ -48,7 +48,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
});
server.route({
method: "GET",
method: "POST",
url: "/public/:id",
config: {
rateLimit: publicEndpointLimit
@@ -57,38 +57,37 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
params: z.object({
id: z.string().uuid()
}),
querystring: z.object({
hashedHex: z.string().min(1)
body: z.object({
hashedHex: z.string().min(1),
password: z.string().optional()
}),
response: {
200: SecretSharingSchema.pick({
encryptedValue: true,
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true,
accessType: true
}).extend({
orgName: z.string().optional()
200: z.object({
isPasswordProtected: z.boolean(),
secret: SecretSharingSchema.pick({
encryptedValue: true,
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true,
accessType: true
})
.extend({
orgName: z.string().optional()
})
.optional()
})
}
},
handler: async (req) => {
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
const sharedSecret = await req.server.services.secretSharing.getSharedSecretById({
sharedSecretId: req.params.id,
hashedHex: req.query.hashedHex,
hashedHex: req.body.hashedHex,
password: req.body.password,
orgId: req.permission?.orgId
});
if (!sharedSecret) return undefined;
return {
encryptedValue: sharedSecret.encryptedValue,
iv: sharedSecret.iv,
tag: sharedSecret.tag,
expiresAt: sharedSecret.expiresAt,
expiresAfterViews: sharedSecret.expiresAfterViews,
accessType: sharedSecret.accessType,
orgName: sharedSecret.orgName
};
return sharedSecret;
}
});
@@ -101,6 +100,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
encryptedValue: z.string(),
password: z.string().optional(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
@@ -131,6 +131,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
name: z.string().max(50).optional(),
password: z.string().optional(),
encryptedValue: z.string(),
hashedHex: z.string(),
iv: z.string(),
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TIdentityAccessTokenDALFactory = ReturnType<typeof identityAccessTokenDALFactory>;
@@ -95,6 +97,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
};
const removeExpiredTokens = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`);
try {
const docs = (tx || db)(TableName.IdentityAccessToken)
.where({
@@ -131,7 +134,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
});
})
.delete();
return await docs;
await docs;
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
} catch (error) {
throw new DatabaseError({ error, name: "IdentityAccessTokenPrune" });
}
@@ -5,6 +5,7 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
@@ -30,7 +31,9 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
let deletedClientSecret: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired univesal auth client secret started`);
do {
try {
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
@@ -71,7 +74,9 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedClientSecret.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired univesal auth client secret completed`);
};
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };
@@ -30,6 +30,7 @@ const getIntegrationSecretsV2 = async (
environment: string;
folderId: string;
depth: number;
secretPath: string;
decryptor: (value: Buffer | null | undefined) => string;
},
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
@@ -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,
@@ -1625,7 +1625,11 @@ const syncSecretsGitHub = async ({
await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: key,
visibility: "all",
visibility: metadata.githubVisibility ?? "all",
...(metadata.githubVisibility === "selected" && {
// we need to map the githubVisibilityRepoIds to numbers
selected_repository_ids: metadata.githubVisibilityRepoIds?.map(Number) ?? []
}),
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
@@ -5,14 +5,18 @@ import { INTEGRATION } from "@app/lib/api-docs";
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
export const IntegrationMetadataSchema = z.object({
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
@@ -20,6 +24,7 @@ export const IntegrationMetadataSchema = z.object({
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
@@ -29,7 +34,15 @@ export const IntegrationMetadataSchema = z.object({
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
githubVisibility: z
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
.optional()
.describe(INTEGRATION.CREATE.metadata.githubVisibility),
githubVisibilityRepoIds: z.array(z.string()).optional().describe(INTEGRATION.CREATE.metadata.githubVisibilityRepoIds),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
@@ -27,6 +27,10 @@ export type TCreateIntegrationDTO = {
key: string;
value: string;
}[];
githubVisibility?: string;
githubVisibilityRepoIds?: string[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
+63
View File
@@ -12,6 +12,7 @@ import {
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
@@ -280,6 +281,67 @@ export const orgDALFactory = (db: TDbClient) => {
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
)
.where({ isGhost: false });
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
const findMembershipWithScimFilter = async (
orgId: string,
scimFilter: string | undefined,
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
) => {
try {
const query = (tx || db.replicaNode())(TableName.OrgMembership)
// eslint-disable-next-line
.where(`${TableName.OrgMembership}.orgId`, orgId)
.where((qb) => {
if (scimFilter) {
void generateKnexQueryFromScim(qb, scimFilter, (attrPath) => {
switch (attrPath) {
case "active":
return `${TableName.OrgMembership}.isActive`;
case "userName":
return `${TableName.UserAliases}.externalId`;
case "name.givenName":
return `${TableName.Users}.firstName`;
case "name.familyName":
return `${TableName.Users}.lastName`;
case "email.value":
return `${TableName.Users}.email`;
default:
return null;
}
});
}
})
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
})
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
@@ -314,6 +376,7 @@ export const orgDALFactory = (db: TDbClient) => {
updateById,
deleteById,
findMembership,
findMembershipWithScimFilter,
createMembership,
updateMembershipById,
deleteMembershipById,
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretFolderVersions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretFolderVersionDALFactory = ReturnType<typeof secretFolderVersionDALFactory>;
@@ -65,6 +67,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions started`);
try {
await db(TableName.SecretFolderVersion)
.with("folder_cte", (qb) => {
@@ -89,6 +92,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
name: "Secret Folder Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
};
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };
@@ -158,9 +158,12 @@ export const fnSecretsV2FromImports = async ({
depth?: number;
cyclicDetector?: Set<string>;
decryptor: (value?: Buffer | null) => string;
expandSecretReferences?: (
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
expandSecretReferences?: (inputSecret: {
value?: string;
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
}) => Promise<string | undefined>;
}) => {
// 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<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// 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 || "";
})
)
)
);
}
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretSharing } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
@@ -30,6 +32,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
};
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired shared secret started`);
try {
const today = new Date();
const docs = await (tx || db)(TableName.SecretSharing)
@@ -40,6 +43,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
tag: "",
iv: ""
});
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired shared secret completed`);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "pruneExpiredSharedSecrets" });
@@ -1,3 +1,6 @@
import bcrypt from "bcrypt";
import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
@@ -36,6 +39,7 @@ export const secretSharingServiceFactory = ({
iv,
tag,
name,
password,
accessType,
expiresAt,
expiresAfterViews
@@ -60,8 +64,10 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Shared secret value too long" });
}
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
name,
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
@@ -77,6 +83,7 @@ export const secretSharingServiceFactory = ({
};
const createPublicSharedSecret = async ({
password,
encryptedValue,
hashedHex,
iv,
@@ -102,7 +109,9 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Shared secret value too long" });
}
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
@@ -111,6 +120,7 @@ export const secretSharingServiceFactory = ({
expiresAfterViews,
accessType
});
return { id: newSharedSecret.id };
};
@@ -152,7 +162,21 @@ export const secretSharingServiceFactory = ({
};
};
const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
const { expiresAfterViews } = sharedSecret;
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
}
await secretSharingDAL.updateById(sharedSecretId, {
lastViewedAt: new Date()
});
};
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = await secretSharingDAL.findOne({
id: sharedSecretId,
hashedHex
@@ -169,6 +193,8 @@ export const secretSharingServiceFactory = ({
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
throw new UnauthorizedError();
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
// or can be safely sent to the client.
if (expiresAt !== null && expiresAt < new Date()) {
// check lifetime expiry
await secretSharingDAL.softDeleteById(sharedSecretId);
@@ -185,21 +211,29 @@ export const secretSharingServiceFactory = ({
});
}
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
const isPasswordProtected = Boolean(sharedSecret.password);
const hasProvidedPassword = Boolean(password);
if (isPasswordProtected) {
if (hasProvidedPassword) {
const isMatch = await bcrypt.compare(password as string, sharedSecret.password as string);
if (!isMatch) throw new UnauthorizedError({ message: "Invalid credentials" });
} else {
return { isPasswordProtected };
}
}
await secretSharingDAL.updateById(sharedSecretId, {
lastViewedAt: new Date()
});
// decrement when we are sure the user will view secret.
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
return {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: undefined
isPasswordProtected,
secret: {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: undefined
}
};
};
@@ -216,6 +250,6 @@ export const secretSharingServiceFactory = ({
createPublicSharedSecret,
getSharedSecrets,
deleteSharedSecretById,
getActiveSharedSecretById
getSharedSecretById
};
};
@@ -15,6 +15,7 @@ export type TSharedSecretPermission = {
orgId: string;
accessType?: SecretSharingAccessType;
name?: string;
password?: string;
};
export type TCreatePublicSharedSecretDTO = {
@@ -24,6 +25,7 @@ export type TCreatePublicSharedSecretDTO = {
tag: string;
expiresAt: string;
expiresAfterViews?: number;
password?: string;
accessType: SecretSharingAccessType;
};
@@ -31,6 +33,11 @@ export type TGetActiveSharedSecretByIdDTO = {
sharedSecretId: string;
hashedHex: string;
orgId?: string;
password?: string;
};
export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
password: string;
};
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
@@ -377,150 +377,116 @@ type TInterpolateSecretArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
const MAX_SECRET_REFERENCE_DEPTH = 10;
export const expandSecretReferencesFactory = ({
projectId,
decryptSecretValue: decryptSecret,
secretDAL,
folderDAL
}: TInterpolateSecretArg) => {
const fetchSecretFactory = () => {
const secretCache: Record<string, Record<string, string>> = {};
const secretCache: Record<string, Record<string, string>> = {};
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<Record<string, string>>((prev, secret) => {
// eslint-disable-next-line
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
return prev;
}, {});
const decryptedSecret = secrets.reduce<Record<string, string>>((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<string, string | undefined>,
interpolatedSec: Record<string, string | undefined>,
fetchSecret: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
): Promise<string | undefined> => {
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<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSecrets: Record<string, string | undefined> = {};
const toBeExpandedSecrets: Record<string, string | undefined> = {};
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<string, boolean> = {};
// 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 = (
@@ -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<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// 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, {
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2BridgeDALFactory>;
@@ -87,6 +89,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 started`);
try {
await db(TableName.SecretVersionV2)
.with("version_cte", (qb) => {
@@ -112,6 +115,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
name: "Secret Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
};
return {
+98 -125
View File
@@ -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<TSecretFolderDALFactory, "findBySecretPath">;
};
const MAX_SECRET_REFERENCE_DEPTH = 5;
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const fetchSecretsCrossEnv = () => {
const fetchCache: Record<string, Record<string, string>> = {};
const secretCache: Record<string, Record<string, string>> = {};
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<Record<string, string>>((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<Record<string, string>>((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<string, string>,
interpolatedSec: Record<string, string>,
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
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<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
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<string, boolean> = {};
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 = (
+70 -53
View File
@@ -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) {
+49 -97
View File
@@ -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;
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
@@ -112,6 +114,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v1 started`);
try {
await db(TableName.SecretVersion)
.with("version_cte", (qb) => {
@@ -137,6 +140,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
name: "Secret Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v1 completed`);
};
return {
+9 -2
View File
@@ -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")
}
}
});