Compare commits

..

1 Commits

Author SHA1 Message Date
Daniel Hougaard
d38243a1c6 Fix: Security attributes for secret lease 2024-08-30 07:14:38 +04:00
552 changed files with 6475 additions and 27836 deletions

View File

@@ -6,15 +6,9 @@ permissions:
contents: read contents: read
jobs: jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-image: infisical-image:
name: Build backend image name: Build backend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [infisical-tests]
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3

1
.gitignore vendored
View File

@@ -63,7 +63,6 @@ yarn-error.log*
# Editor specific # Editor specific
.vscode/* .vscode/*
.idea/*
frontend-build frontend-build

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: 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: [{id:seedData1.id, type: ApproverType.User}],
name: "test-policy"
});
expect(policy.name).toBe("test-policy");
});
});

View File

@@ -1,61 +1,73 @@
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"; 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 () => { describe("Secret Import Router", async () => {
test.each([ test.each([
{ importEnv: "prod", importPath: "/" }, // one in root { importEnv: "prod", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones { importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => { ])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
// check for default environments // check for default environments
const payload = await createSecretImport({ const payload = await createSecretImport(importPath, importEnv);
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath,
importEnv
});
expect(payload).toEqual( expect(payload).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
importPath, importPath: expect.any(String),
importEnv: expect.objectContaining({ importEnv: expect.objectContaining({
name: expect.any(String), name: expect.any(String),
slug: importEnv, slug: expect.any(String),
id: expect.any(String) 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 () => { test("Get secret imports", async () => {
const createdImport1 = await createSecretImport({ const createdImport1 = await createSecretImport("/", "prod");
authToken: jwtAuthToken, const createdImport2 = await createSecretImport("/", "staging");
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({ const res = await testServer.inject({
method: "GET", method: "GET",
url: `/api/v1/secret-imports`, url: `/api/v1/secret-imports`,
@@ -77,60 +89,25 @@ describe("Secret Import Router", async () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
importPath: "/", importPath: expect.any(String),
importEnv: expect.objectContaining({ importEnv: expect.objectContaining({
name: expect.any(String), name: expect.any(String),
slug: "prod", slug: expect.any(String),
id: expect.any(String)
})
}),
expect.objectContaining({
id: expect.any(String),
importPath: "/",
importEnv: expect.objectContaining({
name: expect.any(String),
slug: "staging",
id: expect.any(String) id: expect.any(String)
}) })
}) })
]) ])
); );
await deleteSecretImport({ await deleteSecretImport(createdImport1.id);
id: createdImport1.id, await deleteSecretImport(createdImport2.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 () => { test("Update secret import position", async () => {
const prodImportDetails = { path: "/", envSlug: "prod" }; const prodImportDetails = { path: "/", envSlug: "prod" };
const stagingImportDetails = { path: "/", envSlug: "staging" }; const stagingImportDetails = { path: "/", envSlug: "staging" };
const createdImport1 = await createSecretImport({ const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
authToken: jwtAuthToken, const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
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({ const updateImportRes = await testServer.inject({
method: "PATCH", method: "PATCH",
@@ -184,55 +161,22 @@ describe("Secret Import Router", async () => {
expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id); expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id);
expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id); expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id);
await deleteSecretImport({ await deleteSecretImport(createdImport1.id);
id: createdImport1.id, await deleteSecretImport(createdImport2.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 () => { test("Delete secret import position", async () => {
const createdImport1 = await createSecretImport({ const createdImport1 = await createSecretImport("/", "prod");
authToken: jwtAuthToken, const createdImport2 = await createSecretImport("/", "staging");
secretPath: "/", const deletedImport = await deleteSecretImport(createdImport1.id);
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 // check for default environments
expect(deletedImport).toEqual( expect(deletedImport).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
importPath: "/", importPath: expect.any(String),
importEnv: expect.objectContaining({ importEnv: expect.objectContaining({
name: expect.any(String), name: expect.any(String),
slug: "prod", slug: expect.any(String),
id: expect.any(String) id: expect.any(String)
}) })
}) })
@@ -257,552 +201,6 @@ describe("Secret Import Router", async () => {
expect(secretImportList.secretImports.length).toEqual(1); expect(secretImportList.secretImports.length).toEqual(1);
expect(secretImportList.secretImports[0].position).toEqual(1); expect(secretImportList.secretImports[0].position).toEqual(1);
await deleteSecretImport({ await deleteSecretImport(createdImport2.id);
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"
});
});
}
);

View File

@@ -1,406 +0,0 @@
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 }
);

View File

@@ -1,330 +0,0 @@
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 }
);
});

View File

@@ -8,7 +8,6 @@ type TRawSecret = {
secretComment?: string; secretComment?: string;
version: number; version: number;
}; };
const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => { const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => {
const createSecretReqBody = { const createSecretReqBody = {
workspaceId: seedData1.projectV3.id, workspaceId: seedData1.projectV3.id,

View File

@@ -1,73 +0,0 @@
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[];
};

View File

@@ -1,93 +0,0 @@
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[];
};

View File

@@ -1,128 +0,0 @@
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[];
}[];
};
};

View File

@@ -11,11 +11,10 @@ import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app"; import { main } from "@app/server/app";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { mockQueue } from "./mocks/queue";
import { mockSmtpServer } from "./mocks/smtp"; import { mockSmtpServer } from "./mocks/smtp";
import { mockKeyStore } from "./mocks/keystore";
import { initDbConnection } from "@app/db"; 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 }); dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default { export default {
@@ -29,31 +28,19 @@ export default {
dbRootCert: cfg.DB_ROOT_CERT dbRootCert: cfg.DB_ROOT_CERT
}); });
const redis = new Redis(cfg.REDIS_URL);
await redis.flushdb("SYNC");
try { try {
await db.migrate.rollback(
{
directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts",
tableName: "infisical_migrations"
},
true
);
await db.migrate.latest({ await db.migrate.latest({
directory: path.join(__dirname, "../src/db/migrations"), directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts", extension: "ts",
tableName: "infisical_migrations" tableName: "infisical_migrations"
}); });
await db.seed.run({ await db.seed.run({
directory: path.join(__dirname, "../src/db/seeds"), directory: path.join(__dirname, "../src/db/seeds"),
extension: "ts" extension: "ts"
}); });
const smtp = mockSmtpServer(); const smtp = mockSmtpServer();
const queue = queueServiceFactory(cfg.REDIS_URL); const queue = mockQueue();
const keyStore = keyStoreFactory(cfg.REDIS_URL); const keyStore = mockKeyStore();
const server = await main({ db, smtp, logger, queue, keyStore }); const server = await main({ db, smtp, logger, queue, keyStore });
// @ts-expect-error type // @ts-expect-error type
globalThis.testServer = server; globalThis.testServer = server;
@@ -71,12 +58,10 @@ export default {
{ expiresIn: cfg.JWT_AUTH_LIFETIME } { expiresIn: cfg.JWT_AUTH_LIFETIME }
); );
} catch (error) { } catch (error) {
// eslint-disable-next-line
console.log("[TEST] Error setting up environment", error); console.log("[TEST] Error setting up environment", error);
await db.destroy(); await db.destroy();
throw error; throw error;
} }
// custom setup // custom setup
return { return {
async teardown() { async teardown() {
@@ -95,9 +80,6 @@ export default {
}, },
true true
); );
await redis.flushdb("ASYNC");
redis.disconnect();
await db.destroy(); await db.destroy();
} }
}; };

1182
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,6 @@
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", "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:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", "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", "migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts", "seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
@@ -103,6 +102,7 @@
"tsup": "^8.0.1", "tsup": "^8.0.1",
"tsx": "^4.4.0", "tsx": "^4.4.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.2.2" "vitest": "^1.2.2"
}, },
"dependencies": { "dependencies": {
@@ -112,7 +112,6 @@
"@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-secrets-manager": "^3.504.0",
"@aws-sdk/client-sts": "^3.600.0", "@aws-sdk/client-sts": "^3.600.0",
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@elastic/elasticsearch": "^8.15.0",
"@fastify/cookie": "^9.3.1", "@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.5.0", "@fastify/cors": "^8.5.0",
"@fastify/etag": "^5.1.0", "@fastify/etag": "^5.1.0",
@@ -131,8 +130,6 @@
"@peculiar/x509": "^1.12.1", "@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1", "@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
@@ -160,7 +157,6 @@
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1",
"ms": "^2.1.3", "ms": "^2.1.3",
"mysql2": "^3.9.8", "mysql2": "^3.9.8",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
@@ -178,10 +174,8 @@
"pino": "^8.16.2", "pino": "^8.16.2",
"pkijs": "^3.2.4", "pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.3.8", "probot": "^13.0.0",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",

View File

@@ -70,14 +70,12 @@ import { TSecretReplicationServiceFactory } from "@app/services/secret-replicati
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service"; import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service"; import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service"; import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
declare module "fastify" { declare module "fastify" {
interface FastifyRequest { interface FastifyRequest {
@@ -179,8 +177,6 @@ declare module "fastify" {
userEngagement: TUserEngagementServiceFactory; userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory; externalKms: TExternalKmsServiceFactory;
orgAdmin: TOrgAdminServiceFactory; orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer

View File

@@ -193,9 +193,6 @@ import {
TProjectRolesUpdate, TProjectRolesUpdate,
TProjects, TProjects,
TProjectsInsert, TProjectsInsert,
TProjectSlackConfigs,
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectsUpdate, TProjectsUpdate,
TProjectUserAdditionalPrivilege, TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert, TProjectUserAdditionalPrivilegeInsert,
@@ -302,9 +299,6 @@ import {
TServiceTokens, TServiceTokens,
TServiceTokensInsert, TServiceTokensInsert,
TServiceTokensUpdate, TServiceTokensUpdate,
TSlackIntegrations,
TSlackIntegrationsInsert,
TSlackIntegrationsUpdate,
TSuperAdmin, TSuperAdmin,
TSuperAdminInsert, TSuperAdminInsert,
TSuperAdminUpdate, TSuperAdminUpdate,
@@ -328,10 +322,7 @@ import {
TUsersUpdate, TUsersUpdate,
TWebhooks, TWebhooks,
TWebhooksInsert, TWebhooksInsert,
TWebhooksUpdate, TWebhooksUpdate
TWorkflowIntegrations,
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import { import {
TSecretV2TagJunction, TSecretV2TagJunction,
@@ -785,20 +776,5 @@ declare module "knex/types/tables" {
TKmsKeyVersionsInsert, TKmsKeyVersionsInsert,
TKmsKeyVersionsUpdate TKmsKeyVersionsUpdate
>; >;
[TableName.SlackIntegrations]: KnexOriginal.CompositeTableType<
TSlackIntegrations,
TSlackIntegrationsInsert,
TSlackIntegrationsUpdate
>;
[TableName.ProjectSlackConfigs]: KnexOriginal.CompositeTableType<
TProjectSlackConfigs,
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate
>;
[TableName.WorkflowIntegrations]: KnexOriginal.CompositeTableType<
TWorkflowIntegrations,
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
>;
} }
} }

View File

@@ -115,14 +115,7 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore because generate schema happens after this // @ts-ignore because generate schema happens after this
approverId: knex(TableName.ProjectMembership) approverId: knex(TableName.ProjectMembership)
.join( .select("id")
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`])) .where("userId", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverUserId`]))
}); });
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
@@ -154,27 +147,13 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore because generate schema happens after this // @ts-ignore because generate schema happens after this
committerId: knex(TableName.ProjectMembership) committerId: knex(TableName.ProjectMembership)
.join( .select("id")
TableName.SecretApprovalPolicy, .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])),
`${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 // eslint-disable-next-line
// @ts-ignore because generate schema happens after this // @ts-ignore because generate schema happens after this
statusChangeBy: knex(TableName.ProjectMembership) statusChangeBy: knex(TableName.ProjectMembership)
.join( .select("id")
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`])) .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangedByUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
}); });
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => { await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
@@ -198,20 +177,8 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore because generate schema happens after this // @ts-ignore because generate schema happens after this
member: knex(TableName.ProjectMembership) member: knex(TableName.ProjectMembership)
.join( .select("id")
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`])) .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
}); });
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => { await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
tb.uuid("member").notNullable().alter(); tb.uuid("member").notNullable().alter();

View File

@@ -1,25 +0,0 @@
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");
});
}
}
}

View File

@@ -1,96 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.WorkflowIntegrations))) {
await knex.schema.createTable(TableName.WorkflowIntegrations, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("integration").notNullable();
tb.string("slug").notNullable();
tb.uuid("orgId").notNullable();
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
tb.string("description");
tb.unique(["orgId", "slug"]);
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.WorkflowIntegrations);
}
if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) {
await knex.schema.createTable(TableName.SlackIntegrations, (tb) => {
tb.uuid("id", { primaryKey: true }).notNullable();
tb.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE");
tb.string("teamId").notNullable();
tb.string("teamName").notNullable();
tb.string("slackUserId").notNullable();
tb.string("slackAppId").notNullable();
tb.binary("encryptedBotAccessToken").notNullable();
tb.string("slackBotId").notNullable();
tb.string("slackBotUserId").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.SlackIntegrations);
}
if (!(await knex.schema.hasTable(TableName.ProjectSlackConfigs))) {
await knex.schema.createTable(TableName.ProjectSlackConfigs, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("projectId").notNullable().unique();
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
tb.uuid("slackIntegrationId").notNullable();
tb.foreign("slackIntegrationId").references("id").inTable(TableName.SlackIntegrations).onDelete("CASCADE");
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
tb.string("accessRequestChannels").notNullable().defaultTo("");
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
tb.string("secretRequestChannels").notNullable().defaultTo("");
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
}
const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId");
const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedSlackClientSecret"
);
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
if (!doesSuperAdminHaveSlackClientId) {
tb.binary("encryptedSlackClientId");
}
if (!doesSuperAdminHaveSlackClientSecret) {
tb.binary("encryptedSlackClientSecret");
}
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs);
await dropOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
await knex.schema.dropTableIfExists(TableName.SlackIntegrations);
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations);
await knex.schema.dropTableIfExists(TableName.WorkflowIntegrations);
await dropOnUpdateTrigger(knex, TableName.WorkflowIntegrations);
const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId");
const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedSlackClientSecret"
);
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
if (doesSuperAdminHaveSlackClientId) {
tb.dropColumn("encryptedSlackClientId");
}
if (doesSuperAdminHaveSlackClientSecret) {
tb.dropColumn("encryptedSlackClientSecret");
}
});
}

View File

@@ -1,25 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const hasRequireTemplateForIssuanceColumn = await knex.schema.hasColumn(
TableName.CertificateAuthority,
"requireTemplateForIssuance"
);
if (!hasRequireTemplateForIssuanceColumn) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false);
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("requireTemplateForIssuance");
});
}
}

View File

@@ -1,85 +0,0 @@
import { Knex } from "knex";
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// Certificate template
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (tb) => {
if (!hasKeyUsagesCol) {
tb.specificType("keyUsages", "text[]");
}
if (!hasExtendedKeyUsagesCol) {
tb.specificType("extendedKeyUsages", "text[]");
}
});
if (!hasKeyUsagesCol) {
await knex(TableName.CertificateTemplate).update({
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
});
}
if (!hasExtendedKeyUsagesCol) {
await knex(TableName.CertificateTemplate).update({
extendedKeyUsages: []
});
}
// Certificate
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.Certificate, (tb) => {
if (!doesCertTableHaveKeyUsages) {
tb.specificType("keyUsages", "text[]");
}
if (!doesCertTableHaveExtendedKeyUsages) {
tb.specificType("extendedKeyUsages", "text[]");
}
});
if (!doesCertTableHaveKeyUsages) {
await knex(TableName.Certificate).update({
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
});
}
if (!doesCertTableHaveExtendedKeyUsages) {
await knex(TableName.Certificate).update({
extendedKeyUsages: []
});
}
}
export async function down(knex: Knex): Promise<void> {
// Certificate Template
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (t) => {
if (hasKeyUsagesCol) {
t.dropColumn("keyUsages");
}
if (hasExtendedKeyUsagesCol) {
t.dropColumn("extendedKeyUsages");
}
});
// Certificate
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.Certificate, (t) => {
if (doesCertTableHaveKeyUsages) {
t.dropColumn("keyUsages");
}
if (doesCertTableHaveExtendedKeyUsages) {
t.dropColumn("extendedKeyUsages");
}
});
}

View File

@@ -1,36 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
// add column approverGroupId to AccessApprovalPolicyApprover
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
// make nullable
table.uuid("approverGroupId").nullable().references("id").inTable(TableName.Groups).onDelete("CASCADE");
// make approverUserId nullable
table.uuid("approverUserId").nullable().alter();
});
// add column approverGroupId to SecretApprovalPolicyApprover
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
table.uuid("approverGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
table.uuid("approverUserId").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover)) {
// remove
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (table) => {
table.dropColumn("approverGroupId");
table.uuid("approverUserId").notNullable().alter();
});
// remove
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (table) => {
table.dropColumn("approverGroupId");
table.uuid("approverUserId").notNullable().alter();
});
}
}

View File

@@ -12,8 +12,7 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
policyId: z.string().uuid(), policyId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid().nullable().optional(), approverUserId: z.string().uuid()
approverGroupId: z.string().uuid().nullable().optional()
}); });
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>; export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

View File

@@ -28,8 +28,7 @@ export const CertificateAuthoritiesSchema = z.object({
keyAlgorithm: z.string(), keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(), notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional(), notAfter: z.date().nullable().optional(),
activeCaCertId: z.string().uuid().nullable().optional(), activeCaCertId: z.string().uuid().nullable().optional()
requireTemplateForIssuance: z.boolean().default(false)
}); });
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>; export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;

View File

@@ -16,9 +16,7 @@ export const CertificateTemplatesSchema = z.object({
subjectAlternativeName: z.string(), subjectAlternativeName: z.string(),
ttl: z.string(), ttl: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date()
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional()
}); });
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>; export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;

View File

@@ -22,9 +22,7 @@ export const CertificatesSchema = z.object({
revocationReason: z.number().nullable().optional(), revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional(), altNames: z.string().default("").nullable().optional(),
caCertId: z.string().uuid(), caCertId: z.string().uuid(),
certificateTemplateId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid().nullable().optional()
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;

View File

@@ -62,7 +62,6 @@ export * from "./project-environments";
export * from "./project-keys"; export * from "./project-keys";
export * from "./project-memberships"; export * from "./project-memberships";
export * from "./project-roles"; export * from "./project-roles";
export * from "./project-slack-configs";
export * from "./project-user-additional-privilege"; export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles"; export * from "./project-user-membership-roles";
export * from "./projects"; export * from "./projects";
@@ -102,7 +101,6 @@ export * from "./secret-versions-v2";
export * from "./secrets"; export * from "./secrets";
export * from "./secrets-v2"; export * from "./secrets-v2";
export * from "./service-tokens"; export * from "./service-tokens";
export * from "./slack-integrations";
export * from "./super-admin"; export * from "./super-admin";
export * from "./trusted-ips"; export * from "./trusted-ips";
export * from "./user-actions"; export * from "./user-actions";
@@ -111,4 +109,3 @@ export * from "./user-encryption-keys";
export * from "./user-group-membership"; export * from "./user-group-membership";
export * from "./users"; export * from "./users";
export * from "./webhooks"; export * from "./webhooks";
export * from "./workflow-integrations";

View File

@@ -114,10 +114,7 @@ export enum TableName {
InternalKms = "internal_kms", InternalKms = "internal_kms",
InternalKmsKeyVersion = "internal_kms_key_version", InternalKmsKeyVersion = "internal_kms_key_version",
// @depreciated // @depreciated
KmsKeyVersion = "kms_key_versions", KmsKeyVersion = "kms_key_versions"
WorkflowIntegrations = "workflow_integrations",
SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs"
} }
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@@ -1,24 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectSlackConfigsSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
slackIntegrationId: z.string().uuid(),
isAccessRequestNotificationEnabled: z.boolean().default(false),
accessRequestChannels: z.string().default(""),
isSecretRequestNotificationEnabled: z.boolean().default(false),
secretRequestChannels: z.string().default(""),
createdAt: z.date(),
updatedAt: z.date()
});
export type TProjectSlackConfigs = z.infer<typeof ProjectSlackConfigsSchema>;
export type TProjectSlackConfigsInsert = Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>;
export type TProjectSlackConfigsUpdate = Partial<Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>>;

View File

@@ -12,8 +12,7 @@ export const SecretApprovalPoliciesApproversSchema = z.object({
policyId: z.string().uuid(), policyId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid().nullable().optional(), approverUserId: z.string().uuid()
approverGroupId: z.string().uuid().nullable().optional()
}); });
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>; export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;

View File

@@ -21,8 +21,7 @@ export const SecretSharingSchema = z.object({
expiresAfterViews: z.number().nullable().optional(), expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"), accessType: z.string().default("anyone"),
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional(), lastViewedAt: z.date().nullable().optional()
password: z.string().nullable().optional()
}); });
export type TSecretSharing = z.infer<typeof SecretSharingSchema>; export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

@@ -1,27 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SlackIntegrationsSchema = z.object({
id: z.string().uuid(),
teamId: z.string(),
teamName: z.string(),
slackUserId: z.string(),
slackAppId: z.string(),
encryptedBotAccessToken: zodBuffer,
slackBotId: z.string(),
slackBotUserId: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSlackIntegrations = z.infer<typeof SlackIntegrationsSchema>;
export type TSlackIntegrationsInsert = Omit<z.input<typeof SlackIntegrationsSchema>, TImmutableDBKeys>;
export type TSlackIntegrationsUpdate = Partial<Omit<z.input<typeof SlackIntegrationsSchema>, TImmutableDBKeys>>;

View File

@@ -5,8 +5,6 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const SuperAdminSchema = z.object({ export const SuperAdminSchema = z.object({
@@ -21,9 +19,7 @@ export const SuperAdminSchema = z.object({
trustLdapEmails: z.boolean().default(false).nullable().optional(), trustLdapEmails: z.boolean().default(false).nullable().optional(),
trustOidcEmails: z.boolean().default(false).nullable().optional(), trustOidcEmails: z.boolean().default(false).nullable().optional(),
defaultAuthOrgId: z.string().uuid().nullable().optional(), defaultAuthOrgId: z.string().uuid().nullable().optional(),
enabledLoginMethods: z.string().array().nullable().optional(), enabledLoginMethods: z.string().array().nullable().optional()
encryptedSlackClientId: zodBuffer.nullable().optional(),
encryptedSlackClientSecret: zodBuffer.nullable().optional()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@@ -1,22 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const WorkflowIntegrationsSchema = z.object({
id: z.string().uuid(),
integration: z.string(),
slug: z.string(),
orgId: z.string().uuid(),
description: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
export type TWorkflowIntegrationsInsert = Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>;
export type TWorkflowIntegrationsUpdate = Partial<Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>>;

View File

@@ -1,9 +1,7 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -12,32 +10,28 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/", url: "/",
method: "POST", method: "POST",
config: {
rateLimit: writeLimit
},
schema: { schema: {
body: z.object({ body: z
projectSlug: z.string().trim(), .object({
name: z.string().optional(), projectSlug: z.string().trim(),
secretPath: z.string().trim().default("/"), name: z.string().optional(),
environment: z.string(), secretPath: z.string().trim().default("/"),
approvers: z environment: z.string(),
.discriminatedUnion("type", [ approverUserIds: z.string().array().min(1),
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), approvals: z.number().min(1).default(1),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
]) })
.array() .refine((data) => data.approvals <= data.approverUserIds.length, {
.min(1, { message: "At least one approver should be provided" }), path: ["approvals"],
approvals: z.number().min(1).default(1), message: "The number of approvals should be lower than the number of approvers."
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }),
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({ const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -56,9 +50,6 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/", url: "/",
method: "GET", method: "GET",
config: {
rateLimit: readLimit
},
schema: { schema: {
querystring: z.object({ querystring: z.object({
projectSlug: z.string().trim() projectSlug: z.string().trim()
@@ -67,15 +58,14 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
approvals: sapPubSchema approvals: sapPubSchema
.extend({ .extend({
approvers: z userApprovers: z
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() }) .object({
.array() userId: z.string()
.nullable() })
.optional() .array(),
secretPath: z.string().optional().nullable()
}) })
.array() .array()
.nullable()
.optional()
}) })
} }
}, },
@@ -125,37 +115,33 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/:policyId", url: "/:policyId",
method: "PATCH", method: "PATCH",
config: {
rateLimit: writeLimit
},
schema: { schema: {
params: z.object({ params: z.object({
policyId: z.string() policyId: z.string()
}), }),
body: z.object({ body: z
name: z.string().optional(), .object({
secretPath: z name: z.string().optional(),
.string() secretPath: z
.trim() .string()
.optional() .trim()
.transform((val) => (val === "" ? "/" : val)), .optional()
approvers: z .transform((val) => (val === "" ? "/" : val)),
.discriminatedUnion("type", [ approverUserIds: z.string().array().min(1),
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), approvals: z.number().min(1).default(1),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
]) })
.array() .refine((data) => data.approvals <= data.approverUserIds.length, {
.min(1, { message: "At least one approver should be provided" }), path: ["approvals"],
approvals: z.number().min(1).optional(), message: "The number of approvals should be lower than the number of approvers."
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }),
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({ await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
policyId: req.params.policyId, policyId: req.params.policyId,
@@ -171,9 +157,6 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
server.route({ server.route({
url: "/:policyId", url: "/:policyId",
method: "DELETE", method: "DELETE",
config: {
rateLimit: writeLimit
},
schema: { schema: {
params: z.object({ params: z.object({
policyId: z.string() policyId: z.string()
@@ -184,7 +167,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({ const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -196,44 +179,4 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
return { approval }; return { approval };
} }
}); });
server.route({
url: "/:policyId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
policyId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema.extend({
approvers: z
.object({
type: z.nativeEnum(ApproverType),
id: z.string().nullable().optional(),
name: z.string().nullable().optional()
})
.array()
.nullable()
.optional()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.accessApprovalPolicy.getAccessApprovalPolicyById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { approval };
}
});
}; };

View File

@@ -11,30 +11,6 @@ export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: {
description: "Get CRL in DER format (deprecated)",
params: z.object({
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}),
response: {
200: z.instanceof(Buffer)
}
},
handler: async (req, res) => {
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
res.header("Content-Type", "application/pkix-crl");
return Buffer.from(crl);
}
});
server.route({
method: "GET",
url: "/:crlId/der",
config: {
rateLimit: readLimit
},
schema: { schema: {
description: "Get CRL in DER format", description: "Get CRL in DER format",
params: z.object({ params: z.object({

View File

@@ -2,7 +2,7 @@ import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; import { DEFAULT_REQUEST_SCHEMA, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates"; import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -18,6 +18,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
body: z.object({ body: z.object({
dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(), dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(),
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug), projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug),
@@ -65,6 +66,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
params: z.object({ params: z.object({
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId) leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.DELETE.leaseId)
}), }),
@@ -107,6 +109,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
params: z.object({ params: z.object({
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId) leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.leaseId)
}), }),
@@ -160,6 +163,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
params: z.object({ params: z.object({
leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId) leaseId: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.GET_BY_LEASEID.leaseId)
}), }),

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models"; import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
import { DYNAMIC_SECRETS } from "@app/lib/api-docs"; import { DEFAULT_REQUEST_SCHEMA, DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates"; import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -20,6 +20,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
body: z.object({ body: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug), projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.CREATE.projectSlug),
provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider), provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider),
@@ -77,39 +78,6 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
} }
}); });
server.route({
method: "POST",
url: "/entra-id/users",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
}),
response: {
200: z
.object({
name: z.string().min(1).describe("The name of the user"),
id: z.string().min(1).describe("The ID of the user"),
email: z.string().min(1).describe("The email of the user")
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
tenantId: req.body.tenantId,
applicationId: req.body.applicationId,
clientSecret: req.body.clientSecret
});
return data;
}
});
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:name", url: "/:name",
@@ -117,6 +85,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
params: z.object({ params: z.object({
name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name) name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.UPDATE.name)
}), }),
@@ -184,6 +153,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
params: z.object({ params: z.object({
name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name) name: z.string().toLowerCase().describe(DYNAMIC_SECRETS.DELETE.name)
}), }),
@@ -220,6 +190,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
params: z.object({ params: z.object({
name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name) name: z.string().min(1).describe(DYNAMIC_SECRETS.GET_BY_NAME.name)
}), }),
@@ -257,6 +228,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
querystring: z.object({ querystring: z.object({
projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug), projectSlug: z.string().min(1).describe(DYNAMIC_SECRETS.LIST.projectSlug),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.LIST.path), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRETS.LIST.path),
@@ -270,7 +242,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const dynamicSecretCfgs = await server.services.dynamicSecret.listDynamicSecretsByEnv({ const dynamicSecretCfgs = await server.services.dynamicSecret.list({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -288,6 +260,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
...DEFAULT_REQUEST_SCHEMA,
params: z.object({ params: z.object({
name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name) name: z.string().min(1).describe(DYNAMIC_SECRETS.LIST_LEAES_BY_NAME.name)
}), }),

View File

@@ -10,7 +10,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
url: "/", url: "/",
method: "POST", method: "POST",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
body: z.object({ body: z.object({
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name), name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
@@ -43,59 +43,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}); });
server.route({ server.route({
url: "/:id", url: "/:currentSlug",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
params: z.object({
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
}),
response: {
200: GroupsSchema
}
},
handler: async (req) => {
const group = await server.services.group.getGroupById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.id
});
return group;
}
});
server.route({
url: "/",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
response: {
200: GroupsSchema.array()
}
},
handler: async (req) => {
const groups = await server.services.org.getOrgGroups({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return groups;
}
});
server.route({
url: "/:id",
method: "PATCH", method: "PATCH",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
params: z.object({ params: z.object({
id: z.string().trim().describe(GROUPS.UPDATE.id) currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug)
}), }),
body: z body: z
.object({ .object({
@@ -117,7 +70,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const group = await server.services.group.updateGroup({ const group = await server.services.group.updateGroup({
id: req.params.id, currentSlug: req.params.currentSlug,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -130,12 +83,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}); });
server.route({ server.route({
url: "/:id", url: "/:slug",
method: "DELETE", method: "DELETE",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
params: z.object({ params: z.object({
id: z.string().trim().describe(GROUPS.DELETE.id) slug: z.string().trim().describe(GROUPS.DELETE.slug)
}), }),
response: { response: {
200: GroupsSchema 200: GroupsSchema
@@ -143,7 +96,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const group = await server.services.group.deleteGroup({ const group = await server.services.group.deleteGroup({
id: req.params.id, groupSlug: req.params.slug,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -156,11 +109,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:id/users", url: "/:slug/users",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
params: z.object({ params: z.object({
id: z.string().trim().describe(GROUPS.LIST_USERS.id) slug: z.string().trim().describe(GROUPS.LIST_USERS.slug)
}), }),
querystring: z.object({ querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset), offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
@@ -188,25 +141,24 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const { users, totalCount } = await server.services.group.listGroupUsers({ const { users, totalCount } = await server.services.group.listGroupUsers({
id: req.params.id, groupSlug: req.params.slug,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.query ...req.query
}); });
return { users, totalCount }; return { users, totalCount };
} }
}); });
server.route({ server.route({
method: "POST", method: "POST",
url: "/:id/users/:username", url: "/:slug/users/:username",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
params: z.object({ params: z.object({
id: z.string().trim().describe(GROUPS.ADD_USER.id), slug: z.string().trim().describe(GROUPS.ADD_USER.slug),
username: z.string().trim().describe(GROUPS.ADD_USER.username) username: z.string().trim().describe(GROUPS.ADD_USER.username)
}), }),
response: { response: {
@@ -221,7 +173,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const user = await server.services.group.addUserToGroup({ const user = await server.services.group.addUserToGroup({
id: req.params.id, groupSlug: req.params.slug,
username: req.params.username, username: req.params.username,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -235,11 +187,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:id/users/:username", url: "/:slug/users/:username",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
params: z.object({ params: z.object({
id: z.string().trim().describe(GROUPS.DELETE_USER.id), slug: z.string().trim().describe(GROUPS.DELETE_USER.slug),
username: z.string().trim().describe(GROUPS.DELETE_USER.username) username: z.string().trim().describe(GROUPS.DELETE_USER.username)
}), }),
response: { response: {
@@ -254,7 +206,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const user = await server.services.group.removeUserFromGroup({ const user = await server.services.group.removeUserFromGroup({
id: req.params.id, groupSlug: req.params.slug,
username: req.params.username, username: req.params.username,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
// TODO(akhilmhdh): Fix this when license service gets it type // TODO(akhilmhdh): Fix this when licence service gets it type
import { z } from "zod"; import { z } from "zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";

View File

@@ -101,7 +101,6 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid" message: "Slug must be a valid"
}), }),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}), }),
response: { response: {

View File

@@ -87,12 +87,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
/*
* Daniel: This endpoint is no longer is use.
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
*
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
*/
server.route({ server.route({
method: "GET", method: "GET",
url: "/:workspaceId/audit-logs", url: "/:workspaceId/audit-logs",
@@ -107,7 +101,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId) workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId)
}), }),
querystring: z.object({ querystring: z.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
@@ -128,12 +122,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}) })
.merge( .merge(
z.object({ z.object({
project: z
.object({
name: z.string(),
slug: z.string()
})
.optional(),
event: z.object({ event: z.object({
type: z.string(), type: z.string(),
metadata: z.any() metadata: z.any()
@@ -150,20 +138,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const auditLogs = await server.services.auditLog.listAuditLogs({ const auditLogs = await server.services.auditLog.listProjectAuditLogs({
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type, projectId: req.params.workspaceId,
...req.query,
filter: { endDate: req.query.endDate,
...req.query, startDate: req.query.startDate || getLastMidnightDateISO(),
projectId: req.params.workspaceId, auditLogActor: req.query.actor,
endDate: req.query.endDate, actor: req.permission.type
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined
}
}); });
return { auditLogs }; return { auditLogs };
} }

View File

@@ -100,34 +100,17 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
async (req, profile, cb) => { async (req, profile, cb) => {
try { try {
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
profile?.email ??
// entra sends data in this format
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
const firstName = (profile.firstName ?? if (!email || !profile.firstName) {
// entra sends data in this format throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
const lastName =
profile.lastName ?? profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastName"];
if (!email || !firstName) {
logger.info(
{
err: new Error("Invalid saml request. Missing email or first name"),
profile
},
`email: ${email} firstName: ${profile.firstName as string}`
);
} }
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
externalId: profile.nameID, externalId: profile.nameID,
email, email,
firstName, firstName: profile.firstName as string,
lastName: lastName as string, lastName: profile.lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
@@ -135,7 +118,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
cb(error as Error); cb(null, {});
} }
}, },
() => {} () => {}

View File

@@ -5,47 +5,22 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
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) => { 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);
}
});
server.route({ server.route({
url: "/scim-tokens", url: "/scim-tokens",
method: "POST", method: "POST",
@@ -152,7 +127,25 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
Resources: z.array(ScimUserSchema), 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()
})
),
itemsPerPage: z.number(), itemsPerPage: z.number(),
schemas: z.array(z.string()), schemas: z.array(z.string()),
startIndex: z.number(), startIndex: z.number(),
@@ -180,7 +173,30 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgMembershipId: z.string().trim() orgMembershipId: z.string().trim()
}), }),
response: { response: {
200: ScimUserSchema 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()
})
)
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -200,12 +216,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
schemas: z.array(z.string()), schemas: z.array(z.string()),
userName: z.string().trim(), userName: z.string().trim(),
name: z name: z.object({
.object({ familyName: z.string().trim(),
familyName: z.string().trim().optional(), givenName: z.string().trim()
givenName: z.string().trim().optional() }),
})
.optional(),
emails: z emails: z
.array( .array(
z.object({ z.object({
@@ -215,10 +229,28 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}) })
) )
.optional(), .optional(),
active: z.boolean().default(true) // displayName: z.string().trim(),
active: z.boolean()
}), }),
response: { response: {
200: ScimUserSchema 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]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -228,8 +260,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const user = await req.server.services.scim.createScimUser({ const user = await req.server.services.scim.createScimUser({
externalId: req.body.userName, externalId: req.body.userName,
email: primaryEmail, email: primaryEmail,
firstName: req.body?.name?.givenName, firstName: req.body.name.givenName,
lastName: req.body?.name?.familyName, lastName: req.body.name.familyName,
orgId: req.permission.orgId orgId: req.permission.orgId
}); });
@@ -259,116 +291,6 @@ 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({ server.route({
url: "/Groups", url: "/Groups",
method: "POST", method: "POST",
@@ -383,10 +305,25 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
display: z.string() display: z.string()
}) })
) )
.optional() .optional() // okta-specific
}), }),
response: { response: {
200: ScimGroupSchema 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()
})
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -407,12 +344,26 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
querystring: z.object({ querystring: z.object({
startIndex: z.coerce.number().default(1), startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20), count: z.coerce.number().default(20),
filter: z.string().trim().optional(), filter: z.string().trim().optional()
excludedAttributes: z.string().trim().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
Resources: z.array(ScimGroupSchema), 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()
})
})
),
itemsPerPage: z.number(), itemsPerPage: z.number(),
schemas: z.array(z.string()), schemas: z.array(z.string()),
startIndex: z.number(), startIndex: z.number(),
@@ -426,8 +377,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId, orgId: req.permission.orgId,
startIndex: req.query.startIndex, startIndex: req.query.startIndex,
filter: req.query.filter, filter: req.query.filter,
limit: req.query.count, limit: req.query.count
isMembersExcluded: req.query.excludedAttributes === "members"
}); });
return groups; return groups;
@@ -442,7 +392,20 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: z.string().trim() groupId: z.string().trim()
}), }),
response: { response: {
200: ScimGroupSchema 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()
})
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -451,7 +414,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId orgId: req.permission.orgId
}); });
return group; return group;
} }
}); });
@@ -475,12 +437,25 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
) )
}), }),
response: { response: {
200: ScimGroupSchema 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()
})
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const group = await req.server.services.scim.replaceScimGroup({ const group = await req.server.services.scim.updateScimGroupNamePut({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
...req.body ...req.body
@@ -502,34 +477,54 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
Operations: z.array( Operations: z.array(
z.union([ z.union([
z.object({ z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]), op: z.union([z.literal("replace"), z.literal("Replace")]),
path: z.string().trim(), value: z.object({
value: z id: z.string().trim(),
.object({ displayName: z.string().trim()
value: z.string() })
})
.array()
.optional()
}), }),
z.object({ z.object({
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]), op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim().optional(), path: z.string().trim()
value: z.any() }),
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()
})
)
}) })
]) ])
) )
}), }),
response: { response: {
200: ScimGroupSchema 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()
})
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const group = await req.server.services.scim.updateScimGroup({ const group = await req.server.services.scim.updateScimGroupNamePatch({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
operations: req.body.Operations operations: req.body.Operations
}); });
return group; return group;
} }
}); });
@@ -555,4 +550,60 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
return group; 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;
}
});
}; };

View File

@@ -1,7 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -17,33 +16,32 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z
workspaceId: z.string(), .object({
name: z.string().optional(), workspaceId: z.string(),
environment: z.string(), name: z.string().optional(),
secretPath: z environment: z.string(),
.string() secretPath: z
.optional() .string()
.nullable() .optional()
.default("/") .nullable()
.transform((val) => (val ? removeTrailingSlash(val) : val)), .default("/")
approvers: z .transform((val) => (val ? removeTrailingSlash(val) : val)),
.discriminatedUnion("type", [ approvers: z.string().array().min(1),
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), approvals: z.number().min(1).default(1),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
]) })
.array() .refine((data) => data.approvals <= data.approvers.length, {
.min(1, { message: "At least one approver should be provided" }), path: ["approvals"],
approvals: z.number().min(1).default(1), message: "The number of approvals should be lower than the number of approvers."
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }),
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -69,31 +67,30 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
params: z.object({ params: z.object({
sapId: z.string() sapId: z.string()
}), }),
body: z.object({ body: z
name: z.string().optional(), .object({
approvers: z name: z.string().optional(),
.discriminatedUnion("type", [ approvers: z.string().array().min(1),
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), approvals: z.number().min(1).default(1),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), name: z.string().optional() }) secretPath: z
]) .string()
.array() .optional()
.min(1, { message: "At least one approver should be provided" }), .nullable()
approvals: z.number().min(1).default(1), .transform((val) => (val ? removeTrailingSlash(val) : val))
secretPath: z .transform((val) => (val === "" ? "/" : val)),
.string() enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
.optional() })
.nullable() .refine((data) => data.approvals <= data.approvers.length, {
.transform((val) => (val ? removeTrailingSlash(val) : val)) path: ["approvals"],
.transform((val) => (val === "" ? "/" : val)), message: "The number of approvals should be lower than the number of approvers."
enforcementLevel: z.nativeEnum(EnforcementLevel).optional() }),
}),
response: { response: {
200: z.object({ 200: z.object({
approval: sapPubSchema approval: sapPubSchema
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -123,7 +120,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({ const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
actor: req.permission.type, actor: req.permission.type,
@@ -150,10 +147,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
approvals: sapPubSchema approvals: sapPubSchema
.extend({ .extend({
approvers: z userApprovers: z
.object({ .object({
id: z.string().nullable().optional(), userId: z.string()
type: z.nativeEnum(ApproverType)
}) })
.array() .array()
}) })
@@ -174,44 +170,6 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
} }
}); });
server.route({
url: "/:sapId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
sapId: z.string()
}),
response: {
200: z.object({
approval: sapPubSchema.extend({
approvers: z
.object({
id: z.string().nullable().optional(),
type: z.nativeEnum(ApproverType),
name: z.string().nullable().optional()
})
.array()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const approval = await server.services.secretApprovalPolicy.getSecretApprovalPolicyById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { approval };
}
});
server.route({ server.route({
url: "/board", url: "/board",
method: "GET", method: "GET",
@@ -228,7 +186,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
200: z.object({ 200: z.object({
policy: sapPubSchema policy: sapPubSchema
.extend({ .extend({
userApprovers: z.object({ userId: z.string().nullable().optional() }).array() userApprovers: z.object({ userId: z.string() }).array()
}) })
.optional() .optional()
}) })

View File

@@ -13,7 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge( const approvalRequestUser = z.object({ userId: z.string() }).merge(
UsersSchema.pick({ UsersSchema.pick({
email: true, email: true,
firstName: true, firstName: true,
@@ -46,11 +46,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
approvals: z.number(), approvals: z.number(),
approvers: z approvers: z.string().array(),
.object({
userId: z.string().nullable().optional()
})
.array(),
secretPath: z.string().optional().nullable(), secretPath: z.string().optional().nullable(),
enforcementLevel: z.string() enforcementLevel: z.string()
}), }),
@@ -58,11 +54,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
environment: z.string(), environment: z.string(),
reviewers: z.object({ userId: z.string(), status: z.string() }).array(), reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
approvers: z approvers: z.string().array()
.object({
userId: z.string().nullable().optional()
})
.array()
}).array() }).array()
}) })
} }

View File

@@ -5,38 +5,22 @@ import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApproverType } from "./access-approval-policy-types";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>; export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
export const accessApprovalPolicyDALFactory = (db: TDbClient) => { export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
const accessApprovalPolicyFindQuery = async ( const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
tx: Knex,
filter: TFindFilter<TAccessApprovalPolicies>,
customFilter?: {
policyId?: string;
}
) => {
const result = await tx(TableName.AccessApprovalPolicy) const result = await tx(TableName.AccessApprovalPolicy)
// eslint-disable-next-line // eslint-disable-next-line
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
.where((qb) => {
if (customFilter?.policyId) {
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
}
})
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.leftJoin( .leftJoin(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName")) .select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId")) .select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
@@ -46,10 +30,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
return result; return result;
}; };
const findById = async (policyId: string, tx?: Knex) => { const findById = async (id: string, tx?: Knex) => {
try { try {
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), { const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: policyId [`${TableName.AccessApprovalPolicy}.id` as "id"]: id
}); });
const formattedDoc = sqlNestRelationships({ const formattedDoc = sqlNestRelationships({
data: doc, data: doc,
@@ -66,18 +50,9 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [ childrenMapper: [
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "userApprovers" as const,
mapper: ({ approverUserId: id }) => ({ mapper: ({ approverUserId }) => ({
id, userId: approverUserId
type: "user"
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
id,
type: "group"
}) })
} }
] ]
@@ -89,15 +64,9 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
} }
}; };
const find = async ( const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
customFilter?: {
policyId?: string;
},
tx?: Knex
) => {
try { try {
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter); const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const formattedDocs = sqlNestRelationships({ const formattedDocs = sqlNestRelationships({
data: docs, data: docs,
@@ -115,19 +84,9 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
childrenMapper: [ childrenMapper: [
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "userApprovers" as const,
mapper: ({ approverUserId: id, approverUsername }) => ({ mapper: ({ approverUserId }) => ({
id, userId: approverUserId
type: ApproverType.User,
name: approverUsername
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
id,
type: ApproverType.Group
}) })
} }
] ]

View File

@@ -2,21 +2,17 @@ import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupDALFactory } from "../group/group-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { verifyApprovers } from "./access-approval-policy-fns"; import { verifyApprovers } from "./access-approval-policy-fns";
import { import {
ApproverType,
TCreateAccessApprovalPolicy, TCreateAccessApprovalPolicy,
TDeleteAccessApprovalPolicy, TDeleteAccessApprovalPolicy,
TGetAccessApprovalPolicyByIdDTO,
TGetAccessPolicyCountByEnvironmentDTO, TGetAccessPolicyCountByEnvironmentDTO,
TListAccessApprovalPoliciesDTO, TListAccessApprovalPoliciesDTO,
TUpdateAccessApprovalPolicy TUpdateAccessApprovalPolicy
@@ -29,8 +25,6 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">;
}; };
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>; export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
@@ -38,11 +32,9 @@ export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprov
export const accessApprovalPolicyServiceFactory = ({ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
groupDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectDAL, projectDAL
userDAL
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({ const createAccessApprovalPolicy = async ({
name, name,
@@ -52,7 +44,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath, secretPath,
actorAuthMethod, actorAuthMethod,
approvals, approvals,
approvers, approverUserIds,
projectSlug, projectSlug,
environment, environment,
enforcementLevel enforcementLevel
@@ -60,21 +52,7 @@ export const accessApprovalPolicyServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new BadRequestError({ message: "Project not found" });
// If there is a group approver people might be added to the group later to meet the approvers quota if (approvals > approverUserIds.length)
const groupApprovers = approvers
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const userApprovers = approvers
.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -91,47 +69,6 @@ export const accessApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new BadRequestError({ message: "Environment not found" }); if (!env) throw new BadRequestError({ message: "Environment not found" });
let approverUserIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find({
$in: {
username: userApproverNames
}
});
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
}
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
const verifyAllApprovers = [...approverUserIds];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
.filter((user) => user.isPartOfGroup)
.map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers);
await verifyApprovers({ await verifyApprovers({
projectId: project.id, projectId: project.id,
orgId: actorOrgId, orgId: actorOrgId,
@@ -139,7 +76,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath, secretPath,
actorAuthMethod, actorAuthMethod,
permissionService, permissionService,
userIds: verifyAllApprovers userIds: approverUserIds
}); });
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
@@ -153,26 +90,13 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
if (approverUserIds.length) { await accessApprovalPolicyApproverDAL.insertMany(
await accessApprovalPolicyApproverDAL.insertMany( approverUserIds.map((userId) => ({
approverUserIds.map((userId) => ({ approverUserId: userId,
approverUserId: userId, policyId: doc.id
policyId: doc.id })),
})), tx
tx );
);
}
if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { ...accessApproval, environment: env, projectId: project.id }; return { ...accessApproval, environment: env, projectId: project.id };
@@ -204,7 +128,7 @@ export const accessApprovalPolicyServiceFactory = ({
const updateAccessApprovalPolicy = async ({ const updateAccessApprovalPolicy = async ({
policyId, policyId,
approvers, approverUserIds,
secretPath, secretPath,
name, name,
actorId, actorId,
@@ -214,29 +138,7 @@ export const accessApprovalPolicyServiceFactory = ({
approvals, approvals,
enforcementLevel enforcementLevel
}: TUpdateAccessApprovalPolicy) => { }: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const userApprovers = approvers
.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
const currentAppovals = approvals || accessApprovalPolicy.approvals;
if (
groupApprovers?.length === 0 &&
userApprovers &&
currentAppovals > userApprovers.length + userApproverNames.length
) {
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
}
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -259,30 +161,7 @@ export const accessApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
if (approverUserIds) {
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (userApprovers.length || userApproverNames.length) {
let userApproverIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find({
$in: {
username: userApproverNames
}
});
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
}
await verifyApprovers({ await verifyApprovers({
projectId: accessApprovalPolicy.projectId, projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId, orgId: actorOrgId,
@@ -290,56 +169,18 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath: doc.secretPath!, secretPath: doc.secretPath!,
actorAuthMethod, actorAuthMethod,
permissionService, permissionService,
userIds: userApproverIds userIds: approverUserIds
}); });
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany( await accessApprovalPolicyApproverDAL.insertMany(
userApproverIds.map((userId) => ({ approverUserIds.map((userId) => ({
approverUserId: userId, approverUserId: userId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
} }
if (groupApprovers) {
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
for (const groupId of groupApprovers) {
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
.filter((user) => user.isPartOfGroup)
.map((user) => user.id);
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
envSlug: accessApprovalPolicy.environment.slug,
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: verifyGroupApprovers
});
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -405,40 +246,11 @@ export const accessApprovalPolicyServiceFactory = ({
return { count: policies.length }; return { count: policies.length };
}; };
const getAccessApprovalPolicyById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
policyId
}: TGetAccessApprovalPolicyByIdDTO) => {
const [policy] = await accessApprovalPolicyDAL.find({}, { policyId });
if (!policy) {
throw new NotFoundError({
message: "Cannot find access approval policy"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
policy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return policy;
};
return { return {
getAccessPolicyCountByEnvSlug, getAccessPolicyCountByEnvSlug,
createAccessApprovalPolicy, createAccessApprovalPolicy,
deleteAccessApprovalPolicy, deleteAccessApprovalPolicy,
updateAccessApprovalPolicy, updateAccessApprovalPolicy,
getAccessApprovalPolicyByProjectSlug, getAccessApprovalPolicyByProjectSlug
getAccessApprovalPolicyById
}; };
}; };

View File

@@ -13,16 +13,11 @@ export type TVerifyApprovers = {
orgId: string; orgId: string;
}; };
export enum ApproverType {
Group = "group",
User = "user"
}
export type TCreateAccessApprovalPolicy = { export type TCreateAccessApprovalPolicy = {
approvals: number; approvals: number;
secretPath: string; secretPath: string;
environment: string; environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approverUserIds: string[];
projectSlug: string; projectSlug: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -31,7 +26,7 @@ export type TCreateAccessApprovalPolicy = {
export type TUpdateAccessApprovalPolicy = { export type TUpdateAccessApprovalPolicy = {
policyId: string; policyId: string;
approvals?: number; approvals?: number;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approverUserIds?: string[];
secretPath?: string; secretPath?: string;
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
@@ -46,10 +41,6 @@ export type TGetAccessPolicyCountByEnvironmentDTO = {
projectSlug: string; projectSlug: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetAccessApprovalPolicyByIdDTO = {
policyId: string;
} & Omit<TProjectPermission, "projectId">;
export type TListAccessApprovalPoliciesDTO = { export type TListAccessApprovalPoliciesDTO = {
projectSlug: string; projectSlug: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@@ -39,12 +39,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("requestedByUser"), db(TableName.Users).as("requestedByUser"),
@@ -65,7 +59,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
) )
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
.select( .select(
db.ref("projectId").withSchema(TableName.Environment), db.ref("projectId").withSchema(TableName.Environment),
@@ -149,12 +142,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
label: "reviewers" as const, label: "reviewers" as const,
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined) mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
}, },
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }, { key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => approverGroupUserId
}
] ]
}); });
@@ -184,28 +172,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`requestedByUser.id` `requestedByUser.id`
) )
.leftJoin( .join(
TableName.AccessApprovalPolicyApprover, TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId` `${TableName.AccessApprovalPolicyApprover}.policyId`
) )
.leftJoin<TUsers>( .join<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"), db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id" "accessApprovalPolicyApproverUser.id"
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`,
"accessApprovalPolicyGroupApproverUser.id"
)
.leftJoin( .leftJoin(
TableName.AccessApprovalRequestReviewer, TableName.AccessApprovalRequestReviewer,
@@ -223,15 +200,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership),
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("username").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"), tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("firstName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"), tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("lastName").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"), tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"), tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
@@ -310,23 +282,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
lastName, lastName,
username username
}) })
},
{
key: "userId",
label: "approvers" as const,
mapper: ({
userId,
approverGroupEmail: email,
approverGroupUsername: username,
approverGroupLastName: lastName,
approverFirstName: firstName
}) => ({
userId,
email,
firstName,
lastName,
username
})
} }
] ]
}); });

View File

@@ -5,20 +5,15 @@ import { ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
@@ -38,10 +33,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">; accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick< projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
TProjectDALFactory,
"checkProjectUpgradeStatus" | "findProjectBySlug" | "findProjectWithOrg" | "findById"
>;
accessApprovalRequestDAL: Pick< accessApprovalRequestDAL: Pick<
TAccessApprovalRequestDALFactory, TAccessApprovalRequestDALFactory,
| "create" | "create"
@@ -58,21 +50,17 @@ type TSecretApprovalRequestServiceFactoryDep = {
TAccessApprovalRequestReviewerDALFactory, TAccessApprovalRequestReviewerDALFactory,
"create" | "find" | "findOne" | "transaction" "create" | "find" | "findOne" | "transaction"
>; >;
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick< userDAL: Pick<
TUserDALFactory, TUserDALFactory,
"findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "find" | "findById" "findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "find" | "findById"
>; >;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
}; };
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>; export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
export const accessApprovalRequestServiceFactory = ({ export const accessApprovalRequestServiceFactory = ({
groupDAL,
projectDAL, projectDAL,
projectEnvDAL, projectEnvDAL,
permissionService, permissionService,
@@ -83,9 +71,7 @@ export const accessApprovalRequestServiceFactory = ({
accessApprovalPolicyApproverDAL, accessApprovalPolicyApproverDAL,
additionalPrivilegeDAL, additionalPrivilegeDAL,
smtpService, smtpService,
userDAL, userDAL
kmsService,
projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const createAccessApprovalRequest = async ({ const createAccessApprovalRequest = async ({
isTemporary, isTemporary,
@@ -127,36 +113,13 @@ export const accessApprovalRequestServiceFactory = ({
}); });
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
const approverIds: string[] = [];
const approverGroupIds: string[] = [];
const approvers = await accessApprovalPolicyApproverDAL.find({ const approvers = await accessApprovalPolicyApproverDAL.find({
policyId: policy.id policyId: policy.id
}); });
approvers.forEach((approver) => {
if (approver.approverUserId) {
approverIds.push(approver.approverUserId);
} else if (approver.approverGroupId) {
approverGroupIds.push(approver.approverGroupId);
}
});
const groupUsers = (
await Promise.all(
approverGroupIds.map((groupApproverId) =>
groupDAL.findAllGroupPossibleMembers({
orgId: actorOrgId,
groupId: groupApproverId
})
)
)
).flat();
approverIds.push(...groupUsers.filter((user) => user.isPartOfGroup).map((user) => user.id));
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
$in: { $in: {
id: [...new Set(approverIds)] id: approvers.map((approver) => approver.approverUserId)
} }
}); });
@@ -203,36 +166,13 @@ export const accessApprovalRequestServiceFactory = ({
tx tx
); );
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`;
await triggerSlackNotification({
projectId: project.id,
projectSlackConfigDAL,
projectDAL,
kmsService,
notification: {
type: SlackTriggerFeature.ACCESS_REQUEST,
payload: {
projectName: project.name,
requesterFullName,
isTemporary,
requesterEmail: requestedByUser.email as string,
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl
}
}
});
await smtpService.sendMail({ await smtpService.sendMail({
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!), recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
subjectLine: "Access Approval Request", subjectLine: "Access Approval Request",
substitutions: { substitutions: {
projectName: project.name, projectName: project.name,
requesterFullName, requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
requesterEmail: requestedByUser.email, requesterEmail: requestedByUser.email,
isTemporary, isTemporary,
...(isTemporary && { ...(isTemporary && {
@@ -241,7 +181,7 @@ export const accessApprovalRequestServiceFactory = ({
secretPath, secretPath,
environment: envSlug, environment: envSlug,
permissions: accessTypes, permissions: accessTypes,
approvalUrl approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
}, },
template: SmtpTemplates.AccessApprovalRequest template: SmtpTemplates.AccessApprovalRequest
}); });

View File

@@ -2,11 +2,10 @@ import { ForbiddenError } from "@casl/ability";
import { RawAxiosRequestHeaders } from "axios"; import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { validateLocalIps } from "@app/lib/validator";
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue"; import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
@@ -45,7 +44,6 @@ export const auditLogStreamServiceFactory = ({
}: TCreateAuditLogStreamDTO) => { }: TCreateAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" }); if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
const appCfg = getConfig();
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams) if (!plan.auditLogStreams)
throw new BadRequestError({ throw new BadRequestError({
@@ -61,9 +59,7 @@ export const auditLogStreamServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
if (appCfg.isCloud) { validateLocalIps(url);
blockLocalAndPrivateIpAddresses(url);
}
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
if (totalStreams.length >= plan.auditLogStreamLimit) { if (totalStreams.length >= plan.auditLogStreamLimit) {
@@ -135,8 +131,7 @@ export const auditLogStreamServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
const appCfg = getConfig(); if (url) validateLocalIps(url);
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url);
// testing connection first // testing connection first
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };

View File

@@ -1,14 +1,10 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { AuditLogsSchema, TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { EventType } from "./audit-log-types";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>; export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@@ -28,104 +24,31 @@ export const auditLogDALFactory = (db: TDbClient) => {
const auditLogOrm = ormify(db, TableName.AuditLog); const auditLogOrm = ormify(db, TableName.AuditLog);
const find = async ( const find = async (
{ { orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery,
orgId,
projectId,
userAgentType,
startDate,
endDate,
limit = 20,
offset = 0,
actorId,
actorType,
eventType,
eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
tx?: Knex tx?: Knex
) => { ) => {
if (!orgId && !projectId) {
throw new Error("Either orgId or projectId must be provided");
}
try { try {
// Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog) const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`) .where(
// eslint-disable-next-line func-names stripUndefinedInWhere({
.where(function () { projectId,
if (orgId) { orgId,
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId); eventType,
} else if (projectId) { actor,
void this.where(`${TableName.AuditLog}.projectId`, projectId); userAgentType
} })
});
if (userAgentType) {
void sqlQuery.where("userAgentType", userAgentType);
}
// Select statements
void sqlQuery
.select(selectAllTableCols(TableName.AuditLog))
.select(
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
) )
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
.orderBy(`${TableName.AuditLog}.createdAt`, "desc"); .orderBy("createdAt", "desc");
// Special case: Filter by actor ID
if (actorId) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
}
// Special case: Filter by key/value pairs in eventMetadata field
if (eventMetadata && Object.keys(eventMetadata).length) {
Object.entries(eventMetadata).forEach(([key, value]) => {
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
});
}
// Filter by actor type
if (actorType) {
void sqlQuery.where("actor", actorType);
}
// Filter by event types
if (eventType?.length) {
void sqlQuery.whereIn("eventType", eventType);
}
// Filter by date range
if (startDate) { if (startDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate); void sqlQuery.where("createdAt", ">=", startDate);
} }
if (endDate) { if (endDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate); void sqlQuery.where("createdAt", "<=", endDate);
} }
const docs = await sqlQuery; const docs = await sqlQuery;
return docs;
return docs.map((doc) => {
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
// This is a quick and dirty way to get around the types.
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
return {
...AuditLogsSchema.parse(doc),
...(projectDoc?.projectSlug && {
project: {
name: projectDoc.projectName,
slug: projectDoc.projectSlug
}
})
};
});
} catch (error) { } catch (error) {
throw new DatabaseError({ error }); throw new DatabaseError({ error });
} }
@@ -139,9 +62,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
const today = new Date(); const today = new Date();
let deletedAuditLogIds: { id: string }[] = []; let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0; let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
do { do {
try { try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog) const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
@@ -163,9 +84,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
setTimeout(resolve, 10); // time to breathe for db setTimeout(resolve, 10); // time to breathe for db
}); });
} }
isRetrying = numberOfRetryOnFailure > 0; } while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
}; };
return { ...auditLogOrm, pruneAuditLog, find }; return { ...auditLogOrm, pruneAuditLog, find };

View File

@@ -3,7 +3,6 @@ import { ForbiddenError } from "@casl/ability";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TAuditLogDALFactory } from "./audit-log-dal"; import { TAuditLogDALFactory } from "./audit-log-dal";
@@ -12,7 +11,7 @@ import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-
type TAuditLogServiceFactoryDep = { type TAuditLogServiceFactoryDep = {
auditLogDAL: TAuditLogDALFactory; auditLogDAL: TAuditLogDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
auditLogQueue: TAuditLogQueueServiceFactory; auditLogQueue: TAuditLogQueueServiceFactory;
}; };
@@ -23,48 +22,38 @@ export const auditLogServiceFactory = ({
auditLogQueue, auditLogQueue,
permissionService permissionService
}: TAuditLogServiceFactoryDep) => { }: TAuditLogServiceFactoryDep) => {
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => { const listProjectAuditLogs = async ({
// Filter logs for specific project userAgentType,
if (filter.projectId) { eventType,
const { permission } = await permissionService.getProjectPermission( offset,
actor, limit,
actorId, endDate,
filter.projectId, startDate,
actorAuthMethod, actor,
actorOrgId actorId,
); actorOrgId,
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); actorAuthMethod,
} else { projectId,
// Organization-wide logs auditLogActor
const { permission } = await permissionService.getOrgPermission( }: TListProjectAuditLogDTO) => {
actor, const { permission } = await permissionService.getProjectPermission(
actorId, actor,
actorOrgId, actorId,
actorAuthMethod, projectId,
actorOrgId actorAuthMethod,
); actorOrgId
);
/** ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
* to the organization level ✅
*/
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
}
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
const auditLogs = await auditLogDAL.find({ const auditLogs = await auditLogDAL.find({
startDate: filter.startDate, startDate,
endDate: filter.endDate, endDate,
limit: filter.limit, limit,
offset: filter.offset, offset,
eventType: filter.eventType, eventType,
userAgentType: filter.userAgentType, userAgentType,
actorId: filter.auditLogActorId, actor: auditLogActor,
actorType: filter.actorType, projectId
eventMetadata: filter.eventMetadata,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
}); });
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({ return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
...el, ...el,
event: { type: logEventType, metadata: eventMetadata }, event: { type: logEventType, metadata: eventMetadata },
@@ -87,6 +76,6 @@ export const auditLogServiceFactory = ({
return { return {
createAuditLog, createAuditLog,
listAuditLogs listProjectAuditLogs
}; };
}; };

View File

@@ -5,23 +5,19 @@ import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export type TListProjectAuditLogDTO = { export type TListProjectAuditLogDTO = {
filter: { auditLogActor?: string;
userAgentType?: UserAgentType; projectId: string;
eventType?: EventType[]; eventType?: string;
offset?: number; startDate?: string;
limit: number; endDate?: string;
endDate?: string; userAgentType?: string;
startDate?: string; limit?: number;
projectId?: string; offset?: number;
auditLogActorId?: string; } & TProjectPermission;
actorType?: ActorType;
eventMetadata?: Record<string, string>;
};
} & Omit<TProjectPermission, "projectId">;
export type TCreateAuditLogDTO = { export type TCreateAuditLogDTO = {
event: Event; event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor; actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
orgId?: string; orgId?: string;
projectId?: string; projectId?: string;
} & BaseAuthData; } & BaseAuthData;
@@ -144,7 +140,6 @@ export enum EventType {
GET_CA_CRLS = "get-certificate-authority-crls", GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert", SIGN_CERT = "sign-cert",
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
GET_CERT = "get-cert", GET_CERT = "get-cert",
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert", REVOKE_CERT = "revoke-cert",
@@ -174,15 +169,7 @@ export enum EventType {
GET_CERTIFICATE_TEMPLATE = "get-certificate-template", GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config", CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_SLACK_INTEGRATION = "get-slack-integration",
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -203,8 +190,6 @@ interface IdentityActorMetadata {
interface ScimClientActorMetadata {} interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
export interface UserActor { export interface UserActor {
type: ActorType.USER; type: ActorType.USER;
metadata: UserActorMetadata; metadata: UserActorMetadata;
@@ -215,11 +200,6 @@ export interface ServiceActor {
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export interface IdentityActor { export interface IdentityActor {
type: ActorType.IDENTITY; type: ActorType.IDENTITY;
metadata: IdentityActorMetadata; metadata: IdentityActorMetadata;
@@ -230,7 +210,7 @@ export interface ScimClientActor {
metadata: ScimClientActorMetadata; metadata: ScimClientActorMetadata;
} }
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor; export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@@ -1212,14 +1192,6 @@ interface SignCert {
}; };
} }
interface GetCaCertificateTemplates {
type: EventType.GET_CA_CERTIFICATE_TEMPLATES;
metadata: {
caId: string;
dn: string;
};
}
interface GetCert { interface GetCert {
type: EventType.GET_CERT; type: EventType.GET_CERT;
metadata: { metadata: {
@@ -1474,73 +1446,6 @@ interface GetCertificateTemplateEstConfig {
}; };
} }
interface AttemptCreateSlackIntegration {
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
metadata: {
slug: string;
description?: string;
};
}
interface AttemptReinstallSlackIntegration {
type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION;
metadata: {
id: string;
};
}
interface UpdateSlackIntegration {
type: EventType.UPDATE_SLACK_INTEGRATION;
metadata: {
id: string;
slug: string;
description?: string;
};
}
interface DeleteSlackIntegration {
type: EventType.DELETE_SLACK_INTEGRATION;
metadata: {
id: string;
};
}
interface GetSlackIntegration {
type: EventType.GET_SLACK_INTEGRATION;
metadata: {
id: string;
};
}
interface UpdateProjectSlackConfig {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
metadata: {
id: string;
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
};
}
interface GetProjectSlackConfig {
type: EventType.GET_PROJECT_SLACK_CONFIG;
metadata: {
id: string;
};
}
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -1642,7 +1547,6 @@ export type Event =
| GetCaCrls | GetCaCrls
| IssueCert | IssueCert
| SignCert | SignCert
| GetCaCertificateTemplates
| GetCert | GetCert
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
@@ -1672,12 +1576,4 @@ export type Event =
| DeleteCertificateTemplate | DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig | CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig | UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig | GetCertificateTemplateEstConfig;
| AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration
| UpdateSlackIntegration
| DeleteSlackIntegration
| GetSlackIntegration
| UpdateProjectSlackConfig
| GetProjectSlackConfig
| IntegrationSyncedEvent;

View File

@@ -1,70 +1,10 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>; export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
export const dynamicSecretDALFactory = (db: TDbClient) => { export const dynamicSecretDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecret); const orm = ormify(db, TableName.DynamicSecret);
return orm;
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
const listDynamicSecretsByFolderIds = async (
{
folderIds,
search,
limit,
offset = 0,
orderBy = SecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC
}: {
folderIds: string[];
search?: string;
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
},
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.whereIn("folderId", folderIds)
.where((bd) => {
if (search) {
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
}
})
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select(
selectAllTableCols(TableName.DynamicSecret),
db.ref("slug").withSchema(TableName.Environment).as("environment"),
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
)
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
if (limit) {
const rankOffset = offset + 1;
return await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", rankOffset)
.andWhere("w.rank", "<", rankOffset + limit);
}
const dynamicSecrets = await query;
return dynamicSecrets;
} catch (error) {
throw new DatabaseError({ error, name: "List dynamic secret multi env" });
}
};
return { ...orm, listDynamicSecretsByFolderIds };
}; };

View File

@@ -6,7 +6,6 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -18,12 +17,9 @@ import {
TCreateDynamicSecretDTO, TCreateDynamicSecretDTO,
TDeleteDynamicSecretDTO, TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO, TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO,
TListDynamicSecretsDTO, TListDynamicSecretsDTO,
TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO TUpdateDynamicSecretDTO
} from "./dynamic-secret-types"; } from "./dynamic-secret-types";
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
type TDynamicSecretServiceFactoryDep = { type TDynamicSecretServiceFactoryDep = {
@@ -35,7 +31,7 @@ type TDynamicSecretServiceFactoryDep = {
"pruneDynamicSecret" | "unsetLeaseRevocation" "pruneDynamicSecret" | "unsetLeaseRevocation"
>; >;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
}; };
@@ -304,104 +300,19 @@ export const dynamicSecretServiceFactory = ({
return { ...dynamicSecretCfg, inputs: providerInputs }; return { ...dynamicSecretCfg, inputs: providerInputs };
}; };
// get unique dynamic secret count across multiple envs const list = async ({
const getCountMultiEnv = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
projectId,
path,
environmentSlugs,
search,
isInternal
}: TListDynamicSecretsMultiEnvDTO) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
)
);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find(
{ $in: { folderId: folders.map((folder) => folder.id) }, $search: search ? { name: `%${search}%` } : undefined },
{ countDistinct: "name" }
);
return Number(dynamicSecretCfg[0]?.count ?? 0);
};
// get dynamic secret count for a single env
const getDynamicSecretCount = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
path,
environmentSlug,
search,
projectId
}: TGetDynamicSecretsCountDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find(
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
{ count: true }
);
return Number(dynamicSecretCfg[0]?.count ?? 0);
};
const listDynamicSecretsByEnv = async ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actorId, actorId,
actor, actor,
projectSlug, projectSlug,
path, path,
environmentSlug, environmentSlug
limit,
offset,
orderBy,
orderDirection = OrderByDirection.ASC,
search,
...params
}: TListDynamicSecretsDTO) => { }: TListDynamicSecretsDTO) => {
let { projectId } = params; const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (!projectId) {
if (!projectSlug) throw new BadRequestError({ message: "Project ID or slug required" });
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
projectId = project.id;
}
const projectId = project.id;
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -417,84 +328,15 @@ export const dynamicSecretServiceFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new BadRequestError({ message: "Folder not found" });
const dynamicSecretCfg = await dynamicSecretDAL.find( const dynamicSecretCfg = await dynamicSecretDAL.find({ folderId: folder.id });
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
{
limit,
offset,
sort: orderBy ? [[orderBy, orderDirection]] : undefined
}
);
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
// get dynamic secrets for multiple envs
const listDynamicSecretsByFolderIds = async ({
actorAuthMethod,
actorOrgId,
actorId,
actor,
path,
environmentSlugs,
projectId,
isInternal,
...params
}: TListDynamicSecretsMultiEnvDTO) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
// verify user has access to each env in request
environmentSlugs.forEach((environmentSlug) =>
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
)
);
}
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
if (!folders.length) throw new BadRequestError({ message: "Folders not found" });
const dynamicSecretCfg = await dynamicSecretDAL.listDynamicSecretsByFolderIds({
folderIds: folders.map((folder) => folder.id),
...params
});
return dynamicSecretCfg;
};
const fetchAzureEntraIdUsers = async ({
tenantId,
applicationId,
clientSecret
}: {
tenantId: string;
applicationId: string;
clientSecret: string;
}) => {
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
tenantId,
applicationId,
clientSecret
);
return azureEntraIdUsers;
};
return { return {
create, create,
updateByName, updateByName,
deleteByName, deleteByName,
getDetails, getDetails,
listDynamicSecretsByEnv, list
listDynamicSecretsByFolderIds,
getDynamicSecretCount,
getCountMultiEnv,
fetchAzureEntraIdUsers
}; };
}; };

View File

@@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { DynamicSecretProviderSchema } from "./providers/models"; import { DynamicSecretProviderSchema } from "./providers/models";
@@ -51,20 +50,5 @@ export type TDetailsDynamicSecretDTO = {
export type TListDynamicSecretsDTO = { export type TListDynamicSecretsDTO = {
path: string; path: string;
environmentSlug: string; environmentSlug: string;
projectSlug?: string; projectSlug: string;
projectId?: string;
offset?: number;
limit?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TListDynamicSecretsMultiEnvDTO = Omit<
TListDynamicSecretsDTO,
"projectId" | "environmentSlug" | "projectSlug"
> & { projectId: string; environmentSlugs: string[]; isInternal?: boolean };
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
projectId: string;
};

View File

@@ -1,138 +0,0 @@
import axios from "axios";
import { customAlphabet } from "nanoid";
import { BadRequestError } from "@app/lib/errors";
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
type User = { name: string; id: string; email: string };
export const AzureEntraIDProvider = (): TDynamicProviderFns & {
fetchAzureEntraIdUsers: (tenantId: string, applicationId: string, clientSecret: string) => Promise<User[]>;
} => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await AzureEntraIDSchema.parseAsync(inputs);
return providerInputs;
};
const getToken = async (
tenantId: string,
applicationId: string,
clientSecret: string
): Promise<{ token?: string; success: boolean }> => {
const response = await axios.post<{ access_token: string }>(
`${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`,
{
grant_type: "client_credentials",
client_id: applicationId,
client_secret: clientSecret,
scope: "https://graph.microsoft.com/.default"
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
if (response.status === 200) {
return { token: response.data.access_token, success: true };
}
return { success: false };
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
return data.success;
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
if (!data.success) {
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
}
const password = generatePassword();
const response = await axios.patch(
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
{
passwordProfile: {
forceChangePasswordNextSignIn: false,
password
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${data.token}`
}
}
);
if (response.status !== 204) {
throw new BadRequestError({ message: "Failed to update password" });
}
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
// Creates a new password
await create(inputs);
return { entityId };
};
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
const data = await getToken(tenantId, applicationId, clientSecret);
if (!data.success) {
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
}
const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>(
`${MSFT_GRAPH_API_URL}/users`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Bearer ${data.token}`
}
}
);
if (response.status !== 200) {
throw new BadRequestError({ message: "Failed to fetch users" });
}
const users = response.data.value.map((user) => {
return {
name: user.displayName,
id: user.id,
email: user.userPrincipalName
};
});
return users;
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew,
fetchAzureEntraIdUsers
};
};

View File

@@ -1,126 +0,0 @@
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 ElasticSearchProvider = (): 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
};
};

View File

@@ -1,12 +1,7 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam"; import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra"; import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search";
import { DynamicSecretProviders } from "./models"; import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db";
import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis"; import { RedisDatabaseProvider } from "./redis";
import { SqlDatabaseProvider } from "./sql-database"; import { SqlDatabaseProvider } from "./sql-database";
@@ -15,10 +10,5 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.Cassandra]: CassandraProvider(), [DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider(), [DynamicSecretProviders.AwsIam]: AwsIamProvider(),
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(), [DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(), [DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider()
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
[DynamicSecretProviders.MongoDB]: MongoDBProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider()
}); });

View File

@@ -7,16 +7,12 @@ export enum SqlProviders {
MsSQL = "mssql" MsSQL = "mssql"
} }
export enum ElasticSearchAuthTypes {
User = "user",
ApiKey = "api-key"
}
export const DynamicSecretRedisDBSchema = z.object({ export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(), host: z.string().trim().toLowerCase(),
port: z.number(), port: z.number(),
username: z.string().trim(), // this is often "default". username: z.string().trim(), // this is often "default".
password: z.string().trim().optional(), password: z.string().trim().optional(),
creationStatement: z.string().trim(), creationStatement: z.string().trim(),
revocationStatement: z.string().trim(), revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(), renewStatement: z.string().trim().optional(),
@@ -34,48 +30,6 @@ export const DynamicSecretAwsElastiCacheSchema = z.object({
ca: z.string().optional() 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 DynamicSecretRabbitMqSchema = z.object({
host: z.string().trim().min(1),
port: z.number(),
tags: z.array(z.string().trim()).default([]),
username: z.string().trim().min(1),
password: z.string().trim().min(1),
ca: z.string().optional(),
virtualHost: z.object({
name: z.string().trim().min(1),
permissions: z.object({
read: z.string().trim().min(1),
write: z.string().trim().min(1),
configure: z.string().trim().min(1)
})
})
});
export const DynamicSecretSqlDBSchema = z.object({ export const DynamicSecretSqlDBSchema = z.object({
client: z.nativeEnum(SqlProviders), client: z.nativeEnum(SqlProviders),
host: z.string().trim().toLowerCase(), host: z.string().trim().toLowerCase(),
@@ -113,78 +67,12 @@ export const DynamicSecretAwsIamSchema = z.object({
policyArns: z.string().trim().optional() 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 const DynamicSecretMongoDBSchema = z.object({
host: z.string().min(1).trim().toLowerCase(),
port: z.number().optional(),
username: z.string().min(1).trim(),
password: z.string().min(1).trim(),
database: z.string().min(1).trim(),
ca: z.string().min(1).optional(),
roles: z
.string()
.array()
.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.'
)
});
export const AzureEntraIDSchema = z.object({
tenantId: z.string().trim().min(1),
userId: z.string().trim().min(1),
email: z.string().trim().min(1),
applicationId: z.string().trim().min(1),
clientSecret: z.string().trim().min(1)
});
export enum DynamicSecretProviders { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
Cassandra = "cassandra", Cassandra = "cassandra",
AwsIam = "aws-iam", AwsIam = "aws-iam",
Redis = "redis", Redis = "redis",
AwsElastiCache = "aws-elasticache", AwsElastiCache = "aws-elasticache"
MongoAtlas = "mongo-db-atlas",
ElasticSearch = "elastic-search",
MongoDB = "mongo-db",
RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -192,12 +80,7 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }), z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }), z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }), 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 }),
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema })
]); ]);
export type TDynamicProviderFns = { export type TDynamicProviderFns = {

View File

@@ -1,146 +0,0 @@
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
};
};

View File

@@ -1,116 +0,0 @@
import { MongoClient } from "mongodb";
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 { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const MongoDBProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
if (
appCfg.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 DynamicSecretMongoDBSchema>) => {
const isSrv = !providerInputs.port;
const uri = isSrv
? `mongodb+srv://${providerInputs.host}`
: `mongodb://${providerInputs.host}:${providerInputs.port}`;
const client = new MongoClient(uri, {
auth: {
username: providerInputs.username,
password: providerInputs.password
},
directConnection: !isSrv,
ca: providerInputs.ca
});
return client;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const isConnected = await client
.db(providerInputs.database)
.command({ ping: 1 })
.then(() => true);
await client.close();
return isConnected;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const db = client.db(providerInputs.database);
await db.command({
createUser: username,
pwd: password,
roles: providerInputs.roles
});
await client.close();
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 db = client.db(providerInputs.database);
await db.command({
dropUser: username
});
await client.close();
return { entityId: username };
};
const renew = async (_inputs: unknown, entityId: string) => {
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -1,172 +0,0 @@
import axios, { Axios } from "axios";
import https from "https";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
type TCreateRabbitMQUser = {
axiosInstance: Axios;
createUser: {
username: string;
password: string;
tags: string[];
};
virtualHost: {
name: string;
permissions: {
read: string;
write: string;
configure: string;
};
};
};
type TDeleteRabbitMqUser = {
axiosInstance: Axios;
usernameToDelete: string;
};
async function createRabbitMqUser({ axiosInstance, createUser, virtualHost }: TCreateRabbitMQUser): Promise<void> {
try {
// Create user
const userUrl = `/users/${createUser.username}`;
const userData = {
password: createUser.password,
tags: createUser.tags.join(",")
};
await axiosInstance.put(userUrl, userData);
// Set permissions for the virtual host
if (virtualHost) {
const permissionData = {
configure: virtualHost.permissions.configure,
write: virtualHost.permissions.write,
read: virtualHost.permissions.read
};
await axiosInstance.put(
`/permissions/${encodeURIComponent(virtualHost.name)}/${createUser.username}`,
permissionData
);
}
} catch (error) {
logger.error(error, "Error creating RabbitMQ user");
throw error;
}
}
async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRabbitMqUser) {
await axiosInstance.delete(`users/${usernameToDelete}`);
return { username: usernameToDelete };
}
export const RabbitMqProvider = (): 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 DynamicSecretRabbitMqSchema.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 DynamicSecretRabbitMqSchema>) => {
const axiosInstance = axios.create({
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
auth: {
username: providerInputs.username,
password: providerInputs.password
},
headers: {
"Content-Type": "application/json"
},
...(providerInputs.ca && {
httpsAgent: new https.Agent({ ca: providerInputs.ca, rejectUnauthorized: false })
})
});
return axiosInstance;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const infoResponse = await connection.get("/whoami").then(() => true);
return infoResponse;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
await createRabbitMqUser({
axiosInstance: connection,
virtualHost: providerInputs.virtualHost,
createUser: {
password,
username,
tags: [...(providerInputs.tags ?? []), "infisical-user"]
}
});
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 deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import handlebars from "handlebars"; import handlebars from "handlebars";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";

View File

@@ -60,7 +60,7 @@ export const groupDALFactory = (db: TDbClient) => {
}; };
// special query // special query
const findAllGroupPossibleMembers = async ({ const findAllGroupMembers = async ({
orgId, orgId,
groupId, groupId,
offset = 0, offset = 0,
@@ -125,7 +125,7 @@ export const groupDALFactory = (db: TDbClient) => {
return { return {
findGroups, findGroups,
findByOrgId, findByOrgId,
findAllGroupPossibleMembers, findAllGroupMembers,
...groupOrm ...groupOrm
}; };
}; };

View File

@@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -21,7 +21,6 @@ import {
TAddUserToGroupDTO, TAddUserToGroupDTO,
TCreateGroupDTO, TCreateGroupDTO,
TDeleteGroupDTO, TDeleteGroupDTO,
TGetGroupByIdDTO,
TListGroupUsersDTO, TListGroupUsersDTO,
TRemoveUserFromGroupDTO, TRemoveUserFromGroupDTO,
TUpdateGroupDTO TUpdateGroupDTO
@@ -30,10 +29,7 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = { type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
groupDAL: Pick< groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
@@ -99,7 +95,7 @@ export const groupServiceFactory = ({
}; };
const updateGroup = async ({ const updateGroup = async ({
id, currentSlug,
name, name,
slug, slug,
role, role,
@@ -125,10 +121,8 @@ export const groupServiceFactory = ({
message: "Failed to update group due to plan restrictio Upgrade plan to update group." message: "Failed to update group due to plan restrictio Upgrade plan to update group."
}); });
const group = await groupDAL.findOne({ orgId: actorOrgId, id }); const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug });
if (!group) { if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
throw new BadRequestError({ message: `Failed to find group with ID ${id}` });
}
let customRole: TOrgRoles | undefined; let customRole: TOrgRoles | undefined;
if (role) { if (role) {
@@ -146,7 +140,8 @@ export const groupServiceFactory = ({
const [updatedGroup] = await groupDAL.update( const [updatedGroup] = await groupDAL.update(
{ {
id: group.id orgId: actorOrgId,
slug: currentSlug
}, },
{ {
name, name,
@@ -163,7 +158,7 @@ export const groupServiceFactory = ({
return updatedGroup; return updatedGroup;
}; };
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
@@ -183,39 +178,15 @@ export const groupServiceFactory = ({
}); });
const [group] = await groupDAL.delete({ const [group] = await groupDAL.delete({
id, orgId: actorOrgId,
orgId: actorOrgId slug: groupSlug
}); });
return group; return group;
}; };
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
if (!actorOrgId) {
throw new BadRequestError({ message: "Failed to read group without organization" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
const group = await groupDAL.findById(id);
if (!group) {
throw new NotFoundError({
message: `Cannot find group with ID ${id}`
});
}
return group;
};
const listGroupUsers = async ({ const listGroupUsers = async ({
id, groupSlug,
offset, offset,
limit, limit,
username, username,
@@ -237,15 +208,15 @@ export const groupServiceFactory = ({
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
orgId: actorOrgId, orgId: actorOrgId,
id slug: groupSlug
}); });
if (!group) if (!group)
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to find group with ID ${id}` message: `Failed to find group with slug ${groupSlug}`
}); });
const users = await groupDAL.findAllGroupPossibleMembers({ const users = await groupDAL.findAllGroupMembers({
orgId: group.orgId, orgId: group.orgId,
groupId: group.id, groupId: group.id,
offset, offset,
@@ -258,7 +229,14 @@ export const groupServiceFactory = ({
return { users, totalCount: count }; return { users, totalCount: count };
}; };
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { const addUserToGroup = async ({
groupSlug,
username,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TAddUserToGroupDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
@@ -273,12 +251,12 @@ export const groupServiceFactory = ({
// check if group with slug exists // check if group with slug exists
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
orgId: actorOrgId, orgId: actorOrgId,
id slug: groupSlug
}); });
if (!group) if (!group)
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to find group with ID ${id}` message: `Failed to find group with slug ${groupSlug}`
}); });
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
@@ -307,7 +285,7 @@ export const groupServiceFactory = ({
}; };
const removeUserFromGroup = async ({ const removeUserFromGroup = async ({
id, groupSlug,
username, username,
actor, actor,
actorId, actorId,
@@ -328,12 +306,12 @@ export const groupServiceFactory = ({
// check if group with slug exists // check if group with slug exists
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
orgId: actorOrgId, orgId: actorOrgId,
id slug: groupSlug
}); });
if (!group) if (!group)
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to find group with ID ${id}` message: `Failed to find group with slug ${groupSlug}`
}); });
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
@@ -364,7 +342,6 @@ export const groupServiceFactory = ({
deleteGroup, deleteGroup,
listGroupUsers, listGroupUsers,
addUserToGroup, addUserToGroup,
removeUserFromGroup, removeUserFromGroup
getGroupById
}; };
}; };

View File

@@ -17,7 +17,7 @@ export type TCreateGroupDTO = {
} & TGenericPermission; } & TGenericPermission;
export type TUpdateGroupDTO = { export type TUpdateGroupDTO = {
id: string; currentSlug: string;
} & Partial<{ } & Partial<{
name: string; name: string;
slug: string; slug: string;
@@ -26,27 +26,23 @@ export type TUpdateGroupDTO = {
TGenericPermission; TGenericPermission;
export type TDeleteGroupDTO = { export type TDeleteGroupDTO = {
id: string; groupSlug: string;
} & TGenericPermission;
export type TGetGroupByIdDTO = {
id: string;
} & TGenericPermission; } & TGenericPermission;
export type TListGroupUsersDTO = { export type TListGroupUsersDTO = {
id: string; groupSlug: string;
offset: number; offset: number;
limit: number; limit: number;
username?: string; username?: string;
} & TGenericPermission; } & TGenericPermission;
export type TAddUserToGroupDTO = { export type TAddUserToGroupDTO = {
id: string; groupSlug: string;
username: string; username: string;
} & TGenericPermission; } & TGenericPermission;
export type TRemoveUserFromGroupDTO = { export type TRemoveUserFromGroupDTO = {
id: string; groupSlug: string;
username: string; username: string;
} & TGenericPermission; } & TGenericPermission;

View File

@@ -41,9 +41,10 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
}; };
// special query // special query
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string, tx?: Knex) => { const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
try { try {
const usernameDocs: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership) const usernameDocs: string[] = await db
.replicaNode()(TableName.UserGroupMembership)
.join( .join(
TableName.GroupProjectMembership, TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`,

View File

@@ -26,10 +26,8 @@ export const getDefaultOnPremFeatures = () => {
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
secretApproval: true, secretApproval: false,
secretRotation: true, secretRotation: true,
caCrl: false caCrl: false
}; };
}; };
export const setupLicenseRequestWithStore = () => {};

View File

@@ -49,15 +49,15 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
pkiEst: false pkiEst: false
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
let token: string; let token: string;
const licenseReq = axios.create({ const licenceReq = axios.create({
baseURL, baseURL,
timeout: 35 * 1000 timeout: 35 * 1000
// signal: AbortSignal.timeout(60 * 1000) // signal: AbortSignal.timeout(60 * 1000)
}); });
const refreshLicense = async () => { const refreshLicence = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
const { const {
data: { token: authToken } data: { token: authToken }
@@ -75,7 +75,7 @@ export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string
return token; return token;
}; };
licenseReq.interceptors.request.use( licenceReq.interceptors.request.use(
(config) => { (config) => {
if (token && config.headers) { if (token && config.headers) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@@ -86,7 +86,7 @@ export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string
(err) => Promise.reject(err) (err) => Promise.reject(err)
); );
licenseReq.interceptors.response.use( licenceReq.interceptors.response.use(
(response) => response, (response) => response,
async (err) => { async (err) => {
const originalRequest = (err as AxiosError).config; const originalRequest = (err as AxiosError).config;
@@ -97,15 +97,15 @@ export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string
(originalRequest as any)._retry = true; // injected (originalRequest as any)._retry = true; // injected
// refresh // refresh
await refreshLicense(); await refreshLicence();
licenseReq.defaults.headers.common.Authorization = `Bearer ${token}`; licenceReq.defaults.headers.common.Authorization = `Bearer ${token}`;
return licenseReq(originalRequest!); return licenceReq(originalRequest!);
} }
return Promise.reject(err); return Promise.reject(err);
} }
); );
return { request: licenseReq, refreshLicense }; return { request: licenceReq, refreshLicence };
}; };

View File

@@ -16,8 +16,8 @@ import { TOrgDALFactory } from "@app/services/org/org-dal";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { getDefaultOnPremFeatures, setupLicenceRequestWithStore } from "./licence-fns";
import { TLicenseDALFactory } from "./license-dal"; import { TLicenseDALFactory } from "./license-dal";
import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns";
import { import {
InstanceType, InstanceType,
TAddOrgPmtMethodDTO, TAddOrgPmtMethodDTO,
@@ -64,13 +64,13 @@ export const licenseServiceFactory = ({
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
const appCfg = getConfig(); const appCfg = getConfig();
const licenseServerCloudApi = setupLicenseRequestWithStore( const licenseServerCloudApi = setupLicenceRequestWithStore(
appCfg.LICENSE_SERVER_URL || "", appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_CLOUD_LOGIN, LICENSE_SERVER_CLOUD_LOGIN,
appCfg.LICENSE_SERVER_KEY || "" appCfg.LICENSE_SERVER_KEY || ""
); );
const licenseServerOnPremApi = setupLicenseRequestWithStore( const licenseServerOnPremApi = setupLicenceRequestWithStore(
appCfg.LICENSE_SERVER_URL || "", appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_ON_PREM_LOGIN, LICENSE_SERVER_ON_PREM_LOGIN,
appCfg.LICENSE_KEY || "" appCfg.LICENSE_KEY || ""
@@ -79,7 +79,7 @@ export const licenseServiceFactory = ({
const init = async () => { const init = async () => {
try { try {
if (appCfg.LICENSE_SERVER_KEY) { if (appCfg.LICENSE_SERVER_KEY) {
const token = await licenseServerCloudApi.refreshLicense(); const token = await licenseServerCloudApi.refreshLicence();
if (token) instanceType = InstanceType.Cloud; if (token) instanceType = InstanceType.Cloud;
logger.info(`Instance type: ${InstanceType.Cloud}`); logger.info(`Instance type: ${InstanceType.Cloud}`);
isValidLicense = true; isValidLicense = true;
@@ -87,7 +87,7 @@ export const licenseServiceFactory = ({
} }
if (appCfg.LICENSE_KEY) { if (appCfg.LICENSE_KEY) {
const token = await licenseServerOnPremApi.refreshLicense(); const token = await licenseServerOnPremApi.refreshLicence();
if (token) { if (token) {
const { const {
data: { currentPlan } data: { currentPlan }

View File

@@ -1,5 +1,7 @@
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
import { conditionsMatcher } from "@app/lib/casl";
export enum OrgPermissionActions { export enum OrgPermissionActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -25,8 +27,7 @@ export enum OrgPermissionSubjects {
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity", Identity = "identity",
Kms = "kms", Kms = "kms",
AdminConsole = "organization-admin-console", AdminConsole = "organization-admin-console"
AuditLogs = "audit-logs"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@@ -44,11 +45,10 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions // ws permissions
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
@@ -113,20 +113,15 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return rules; return build({ conditionsMatcher });
}; };
export const orgAdminPermissions = buildAdminPermission(); export const orgAdminPermissions = buildAdminPermission();
const buildMemberPermission = () => { const buildMemberPermission = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
@@ -147,16 +142,14 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); return build({ conditionsMatcher });
return rules;
}; };
export const orgMemberPermissions = buildMemberPermission(); export const orgMemberPermissions = buildMemberPermission();
const buildNoAccessPermission = () => { const buildNoAccessPermission = () => {
const { rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
return rules; return build({ conditionsMatcher });
}; };
export const orgNoAccessPermissions = buildNoAccessPermission(); export const orgNoAccessPermissions = buildNoAccessPermission();

View File

@@ -1,13 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas";
IdentityProjectMembershipRoleSchema,
OrgMembershipsSchema,
TableName,
TProjectRoles,
TProjects
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@@ -16,92 +10,18 @@ export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
export const permissionDALFactory = (db: TDbClient) => { export const permissionDALFactory = (db: TDbClient) => {
const getOrgPermission = async (userId: string, orgId: string) => { const getOrgPermission = async (userId: string, orgId: string) => {
try { try {
const groupSubQuery = db(TableName.Groups)
.where(`${TableName.Groups}.orgId`, orgId)
.join(TableName.UserGroupMembership, (queryBuilder) => {
queryBuilder
.on(`${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
.andOn(`${TableName.UserGroupMembership}.userId`, db.raw("?", [userId]));
})
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.select(
db.ref("id").withSchema(TableName.Groups).as("groupId"),
db.ref("orgId").withSchema(TableName.Groups).as("groupOrgId"),
db.ref("name").withSchema(TableName.Groups).as("groupName"),
db.ref("slug").withSchema(TableName.Groups).as("groupSlug"),
db.ref("role").withSchema(TableName.Groups).as("groupRole"),
db.ref("roleId").withSchema(TableName.Groups).as("groupRoleId"),
db.ref("createdAt").withSchema(TableName.Groups).as("groupCreatedAt"),
db.ref("updatedAt").withSchema(TableName.Groups).as("groupUpdatedAt"),
db.ref("permissions").withSchema(TableName.OrgRoles).as("groupCustomRolePermission")
);
const membership = await db const membership = await db
.replicaNode()(TableName.OrgMembership) .replicaNode()(TableName.OrgMembership)
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId)
.where(`${TableName.OrgMembership}.orgId`, orgId) .where(`${TableName.OrgMembership}.orgId`, orgId)
.where(`${TableName.OrgMembership}.userId`, userId) .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
.leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`) .select("permissions")
.leftJoin<Awaited<typeof groupSubQuery>[0]>( .select(selectAllTableCols(TableName.OrgMembership))
groupSubQuery.as("userGroups"), .first();
"userGroups.groupOrgId",
db.raw("?", [orgId])
)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.OrgRoles),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("groupId").withSchema("userGroups"),
db.ref("groupOrgId").withSchema("userGroups"),
db.ref("groupName").withSchema("userGroups"),
db.ref("groupSlug").withSchema("userGroups"),
db.ref("groupRole").withSchema("userGroups"),
db.ref("groupRoleId").withSchema("userGroups"),
db.ref("groupCreatedAt").withSchema("userGroups"),
db.ref("groupUpdatedAt").withSchema("userGroups"),
db.ref("groupCustomRolePermission").withSchema("userGroups")
);
const [formatedDoc] = sqlNestRelationships({ return membership;
data: membership,
key: "id",
parentMapper: (el) =>
OrgMembershipsSchema.extend({
permissions: z.unknown(),
orgAuthEnforced: z.boolean().optional().nullable(),
customRoleSlug: z.string().optional().nullable()
}).parse(el),
childrenMapper: [
{
key: "groupId",
label: "groups" as const,
mapper: ({
groupId,
groupUpdatedAt,
groupCreatedAt,
groupRole,
groupRoleId,
groupCustomRolePermission,
groupName,
groupSlug,
groupOrgId
}) => ({
id: groupId,
updatedAt: groupUpdatedAt,
createdAt: groupCreatedAt,
role: groupRole,
roleId: groupRoleId,
customRolePermission: groupCustomRolePermission,
name: groupName,
slug: groupSlug,
orgId: groupOrgId
})
}
]
});
return formatedDoc;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "GetOrgPermission" }); throw new DatabaseError({ error, name: "GetOrgPermission" });
} }
@@ -127,31 +47,74 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => { const getProjectPermission = async (userId: string, projectId: string) => {
try { try {
const docs = await db const groups: string[] = await db
.replicaNode()(TableName.Users) .replicaNode()(TableName.GroupProjectMembership)
.where(`${TableName.Users}.id`, userId) .where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .pluck(`${TableName.GroupProjectMembership}.groupId`);
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
void queryBuilder const groupDocs = await db
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) .replicaNode()(TableName.UserGroupMembership)
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`); .where(`${TableName.UserGroupMembership}.userId`, userId)
}) .whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
.leftJoin( .join(
TableName.GroupProjectMembership,
`${TableName.GroupProjectMembership}.groupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join(
TableName.GroupProjectMembershipRole, TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`, `${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id` `${TableName.GroupProjectMembership}.id`
) )
.leftJoin<TProjectRoles>(
{ groupCustomRoles: TableName.ProjectRoles },
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`groupCustomRoles.id`
)
.leftJoin(TableName.ProjectMembership, (queryBuilder) => {
void queryBuilder
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
})
.leftJoin( .leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.GroupProjectMembership}.projectId`,
`${TableName.Project}.id`
)
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles).as("permissions"),
// db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("apPermissions")
// Additional Privileges
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessEndTime")
);
// .select(`${TableName.ProjectRoles}.permissions`);
const docs = await db(TableName.ProjectMembership)
.join(
TableName.ProjectUserMembershipRole, TableName.ProjectUserMembershipRole,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`, `${TableName.ProjectUserMembershipRole}.projectMembershipId`,
`${TableName.ProjectMembership}.id` `${TableName.ProjectMembership}.id`
@@ -161,229 +124,176 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.ProjectUserMembershipRole}.customRoleId`, `${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id` `${TableName.ProjectRoles}.id`
) )
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { .leftJoin(
void queryBuilder TableName.ProjectUserAdditionalPrivilege,
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) `${TableName.ProjectUserAdditionalPrivilege}.projectId`,
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); `${TableName.ProjectMembership}.projectId`
}) )
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where(`${TableName.ProjectMembership}.userId`, userId)
.where(`${TableName.ProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
.select( .select(
db.ref("id").withSchema(TableName.Users).as("userId"),
// groups specific
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
db
.ref("customRoleId")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleCustomRoleId"),
db
.ref("isTemporary")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleIsTemporary"),
db
.ref("temporaryMode")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryMode"),
db
.ref("temporaryRange")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
// user specific
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), db.ref("projectId").withSchema(TableName.ProjectMembership),
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
db
.ref("temporaryMode")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryMode"),
db
.ref("isTemporary")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
db
.ref("permissions")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesPermissions"),
db
.ref("temporaryMode")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryMode"),
db
.ref("isTemporary")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryRange"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
// general
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project), db.ref("orgId").withSchema(TableName.Project),
db.ref("id").withSchema(TableName.Project).as("projectId") db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles),
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessEndTime")
); );
const [userPermission] = sqlNestRelationships({ const permission = sqlNestRelationships({
data: docs, data: docs,
key: "projectId", key: "projectId",
parentMapper: ({ parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
orgId,
orgAuthEnforced,
membershipId,
groupMembershipId,
membershipCreatedAt,
groupMembershipCreatedAt,
groupMembershipUpdatedAt,
membershipUpdatedAt
}) => ({
orgId, orgId,
orgAuthEnforced, orgAuthEnforced,
userId, userId,
id: membershipId,
projectId, projectId,
id: membershipId || groupMembershipId, createdAt: membershipCreatedAt,
createdAt: membershipCreatedAt || groupMembershipCreatedAt, updatedAt: membershipUpdatedAt
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
}), }),
childrenMapper: [ childrenMapper: [
{ {
key: "userGroupProjectMembershipRoleId", key: "id",
label: "userGroupRoles" as const, label: "roles" as const,
mapper: ({ mapper: (data) =>
userGroupProjectMembershipRoleId, ProjectUserMembershipRolesSchema.extend({
userGroupProjectMembershipRole, permissions: z.unknown(),
userGroupProjectMembershipRolePermission, customRoleSlug: z.string().optional().nullable()
userGroupProjectMembershipRoleCustomRoleSlug, }).parse(data)
userGroupProjectMembershipRoleIsTemporary,
userGroupProjectMembershipRoleTemporaryMode,
userGroupProjectMembershipRoleTemporaryAccessEndTime,
userGroupProjectMembershipRoleTemporaryAccessStartTime,
userGroupProjectMembershipRoleTemporaryRange
}) => ({
id: userGroupProjectMembershipRoleId,
role: userGroupProjectMembershipRole,
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
permissions: userGroupProjectMembershipRolePermission,
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: userGroupProjectMembershipRoleIsTemporary
})
}, },
{ {
key: "userProjectMembershipRoleId", key: "userApId",
label: "projecMembershiptRoles" as const,
mapper: ({
userProjectMembershipRoleId,
userProjectMembershipRole,
userProjectCustomRolePermission,
userProjectMembershipRoleIsTemporary,
userProjectMembershipRoleTemporaryMode,
userProjectMembershipRoleTemporaryRange,
userProjectMembershipRoleTemporaryAccessEndTime,
userProjectMembershipRoleTemporaryAccessStartTime,
userProjectMembershipRoleCustomRoleSlug
}) => ({
id: userProjectMembershipRoleId,
role: userProjectMembershipRole,
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
permissions: userProjectCustomRolePermission,
temporaryRange: userProjectMembershipRoleTemporaryRange,
temporaryMode: userProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: userProjectMembershipRoleIsTemporary
})
},
{
key: "userAdditionalPrivilegesId",
label: "additionalPrivileges" as const, label: "additionalPrivileges" as const,
mapper: ({ mapper: ({
userAdditionalPrivilegesId, userApId,
userAdditionalPrivilegesPermissions, userApPermissions,
userAdditionalPrivilegesIsTemporary, userApIsTemporary,
userAdditionalPrivilegesTemporaryMode, userApTemporaryMode,
userAdditionalPrivilegesTemporaryRange, userApTemporaryRange,
userAdditionalPrivilegesTemporaryAccessEndTime, userApTemporaryAccessEndTime,
userAdditionalPrivilegesTemporaryAccessStartTime userApTemporaryAccessStartTime
}) => ({ }) => ({
id: userAdditionalPrivilegesId, id: userApId,
permissions: userAdditionalPrivilegesPermissions, permissions: userApPermissions,
temporaryRange: userAdditionalPrivilegesTemporaryRange, temporaryRange: userApTemporaryRange,
temporaryMode: userAdditionalPrivilegesTemporaryMode, temporaryMode: userApTemporaryMode,
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, temporaryAccessEndTime: userApTemporaryAccessEndTime,
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, temporaryAccessStartTime: userApTemporaryAccessStartTime,
isTemporary: userAdditionalPrivilegesIsTemporary isTemporary: userApIsTemporary
}) })
} }
] ]
}); });
if (!userPermission) return undefined; const groupPermission = groupDocs.length
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined; ? sqlNestRelationships({
data: groupDocs,
key: "projectId",
parentMapper: ({ orgId, orgAuthEnforced, membershipId, membershipCreatedAt, membershipUpdatedAt }) => ({
orgId,
orgAuthEnforced,
userId,
id: membershipId,
projectId,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt
}),
childrenMapper: [
{
key: "id",
label: "roles" as const,
mapper: (data) =>
ProjectUserMembershipRolesSchema.extend({
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
},
{
key: "userApId",
label: "additionalPrivileges" as const,
mapper: ({
userApId,
userApProjectId,
userApUserId,
userApPermissions,
userApIsTemporary,
userApTemporaryMode,
userApTemporaryRange,
userApTemporaryAccessEndTime,
userApTemporaryAccessStartTime
}) => ({
id: userApId,
userId: userApUserId,
projectId: userApProjectId,
permissions: userApPermissions,
temporaryRange: userApTemporaryRange,
temporaryMode: userApTemporaryMode,
temporaryAccessEndTime: userApTemporaryAccessEndTime,
temporaryAccessStartTime: userApTemporaryAccessStartTime,
isTemporary: userApIsTemporary
})
}
]
})
: [];
if (!permission?.[0] && !groupPermission[0]) return undefined;
// when introducting cron mode change it here // when introducting cron mode change it here
const activeRoles = const activeRoles =
userPermission?.projecMembershiptRoles?.filter( permission?.[0]?.roles?.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? []; ) ?? [];
const activeGroupRoles = const activeGroupRoles =
userPermission?.userGroupRoles?.filter( groupPermission?.[0]?.roles?.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? []; ) ?? [];
const activeAdditionalPrivileges = const activeAdditionalPrivileges =
userPermission?.additionalPrivileges?.filter( permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) => ({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? []; ) ?? [];
const activeGroupAdditionalPrivileges =
groupPermission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime, userId: apUserId, projectId: apProjectId }) =>
apProjectId === projectId &&
apUserId === userId &&
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
) ?? [];
return { return {
...userPermission, ...(permission[0] || groupPermission[0]),
roles: [...activeRoles, ...activeGroupRoles], roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: activeAdditionalPrivileges additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" }); throw new DatabaseError({ error, name: "GetProjectPermission" });

View File

@@ -20,7 +20,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal"; import { TPermissionDALFactory } from "./permission-dal";
import { validateOrgSAML } from "./permission-fns"; import { validateOrgSAML } from "./permission-fns";
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types"; import { TBuildProjectPermissionDTO } from "./permission-types";
import { import {
buildServiceTokenProjectPermission, buildServiceTokenProjectPermission,
projectAdminPermissions, projectAdminPermissions,
@@ -47,29 +47,26 @@ export const permissionServiceFactory = ({
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL
}: TPermissionServiceFactoryDep) => { }: TPermissionServiceFactoryDep) => {
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { const buildOrgPermission = (role: string, permission?: unknown) => {
const rules = orgUserRoles switch (role) {
.map(({ role, permissions }) => { case OrgMembershipRole.Admin:
switch (role) { return orgAdminPermissions;
case OrgMembershipRole.Admin: case OrgMembershipRole.Member:
return orgAdminPermissions; return orgMemberPermissions;
case OrgMembershipRole.Member: case OrgMembershipRole.NoAccess:
return orgMemberPermissions; return orgNoAccessPermissions;
case OrgMembershipRole.NoAccess: case OrgMembershipRole.Custom:
return orgNoAccessPermissions; return createMongoAbility<OrgPermissionSet>(
case OrgMembershipRole.Custom: unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
return unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>( permission as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] ),
); {
default: conditionsMatcher
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" }); }
} );
}) default:
.reduce((curr, prev) => prev.concat(curr), []); throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
}
return createMongoAbility<OrgPermissionSet>(rules, {
conditionsMatcher
});
}; };
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => { const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
@@ -132,13 +129,7 @@ export const permissionServiceFactory = ({
validateOrgSAML(authMethod, membership.orgAuthEnforced); validateOrgSAML(authMethod, membership.orgAuthEnforced);
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
membership?.groups?.map(({ role, customRolePermission }) => ({
role,
permissions: customRolePermission
})) || []
);
return { permission: buildOrgPermission(finalPolicyRoles), membership };
}; };
const getIdentityOrgPermission = async (identityId: string, orgId: string) => { const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
@@ -147,10 +138,7 @@ export const permissionServiceFactory = ({
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom permission not found" }); throw new BadRequestError({ name: "Custom permission not found" });
} }
return { return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]),
membership
};
}; };
const getOrgPermission = async ( const getOrgPermission = async (
@@ -181,11 +169,11 @@ export const permissionServiceFactory = ({
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
if (!orgRole) throw new BadRequestError({ message: "Role not found" }); if (!orgRole) throw new BadRequestError({ message: "Role not found" });
return { return {
permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]), permission: buildOrgPermission(OrgMembershipRole.Custom, orgRole.permissions),
role: orgRole role: orgRole
}; };
} }
return { permission: buildOrgPermission([{ role, permissions: [] }]) }; return { permission: buildOrgPermission(role, []) };
}; };
// user permission for a project in an organization // user permission for a project in an organization
@@ -346,7 +334,7 @@ export const permissionServiceFactory = ({
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
if (isCustomRole) { if (isCustomRole) {
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
if (!projectRole) throw new BadRequestError({ message: `Role not found: ${role}` }); if (!projectRole) throw new BadRequestError({ message: "Role not found" });
return { return {
permission: buildProjectPermission([ permission: buildProjectPermission([
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } { role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }

View File

@@ -2,8 +2,3 @@ export type TBuildProjectPermissionDTO = {
permissions?: unknown; permissions?: unknown;
role: string; role: string;
}[]; }[];
export type TBuildOrgPermissionDTO = {
permissions?: unknown;
role: string;
}[];

View File

@@ -1,7 +1,6 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError } from "@app/lib/errors";
export enum ProjectPermissionActions { export enum ProjectPermissionActions {
Read = "read", Read = "read",
@@ -76,127 +75,117 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions.
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
// double check if all CRUD are needed for CA and Certificates
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
];
const buildAdminPermissionRules = () => { const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
fullProjectPermissionSet.forEach((permission) => { can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
const [action, subject] = permission; can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(action, subject); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
});
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
return rules; return rules;
}; };
@@ -383,31 +372,4 @@ export const isAtLeastAsPrivilegedWorkspace = (
return set1.size >= set2.size; return set1.size >= set2.size;
}; };
/*
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set.
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
*/
export const validateProjectPermissions = (permissions: unknown) => {
const parsedPermissions =
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
const flattenedPermissions = [...parsedPermissions];
for (const perm of flattenedPermissions) {
const [action, subject] = perm;
if (
!fullProjectPermissionSet.find(
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
)
) {
throw new BadRequestError({
message: `Permission action ${action} on subject ${subject} is not valid`,
name: "Create Role"
});
}
}
};
/* eslint-enable */ /* eslint-enable */

View File

@@ -44,18 +44,19 @@ export const buildScimUser = ({
email, email,
firstName, firstName,
lastName, lastName,
active, groups = [],
createdAt, active
updatedAt
}: { }: {
orgMembershipId: string; orgMembershipId: string;
username: string; username: string;
email?: string | null; email?: string | null;
firstName: string | null | undefined; firstName: string | null | undefined;
lastName: string | null | undefined; lastName: string | null | undefined;
groups?: {
value: string;
display: string;
}[];
active: boolean; active: boolean;
createdAt: Date;
updatedAt: Date;
}): TScimUser => { }): TScimUser => {
const scimUser = { const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -77,10 +78,10 @@ export const buildScimUser = ({
] ]
: [], : [],
active, active,
groups,
meta: { meta: {
resourceType: "User", resourceType: "User",
created: createdAt, location: null
lastModified: updatedAt
} }
}; };
@@ -108,18 +109,14 @@ export const buildScimGroupList = ({
export const buildScimGroup = ({ export const buildScimGroup = ({
groupId, groupId,
name, name,
members, members
updatedAt,
createdAt
}: { }: {
groupId: string; groupId: string;
name: string; name: string;
members: { members: {
value: string; value: string;
display?: string; display: string;
}[]; }[];
createdAt: Date;
updatedAt: Date;
}): TScimGroup => { }): TScimGroup => {
const scimGroup = { const scimGroup = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
@@ -128,8 +125,7 @@ export const buildScimGroup = ({
members, members,
meta: { meta: {
resourceType: "Group", resourceType: "Group",
created: createdAt, location: null
lastModified: updatedAt
} }
}; };

View File

@@ -1,7 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -10,6 +9,7 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
@@ -32,7 +32,14 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns"; import {
buildScimGroup,
buildScimGroupList,
buildScimUser,
buildScimUserList,
extractScimValueFromPath,
parseScimFilter
} from "./scim-fns";
import { import {
TCreateScimGroupDTO, TCreateScimGroupDTO,
TCreateScimTokenDTO, TCreateScimTokenDTO,
@@ -57,32 +64,19 @@ type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">; scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick< userDAL: Pick<
TUserDALFactory, TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById" "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
>; >;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete" | "update">; userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
| "createMembership" "createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
| "findById"
| "findMembership"
| "findMembershipWithScimFilter"
| "deleteMembershipById"
| "transaction"
| "updateMembershipById"
>; >;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
| "create" "create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update"
| "findOne"
| "findAllGroupPossibleMembers"
| "delete"
| "findGroups"
| "transaction"
| "updateById"
| "update"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
@@ -199,12 +193,7 @@ export const scimServiceFactory = ({
}; };
// SCIM server endpoints // SCIM server endpoints
const listScimUsers = async ({ const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
startIndex = 0,
limit = 100,
filter,
orgId
}: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId); const org = await orgDAL.findById(orgId);
if (!org.scimEnabled) if (!org.scimEnabled)
@@ -218,20 +207,23 @@ export const scimServiceFactory = ({
...(limit && { limit }) ...(limit && { limit })
}; };
const users = await orgDAL.findMembershipWithScimFilter(orgId, filter, findOpts); const users = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseScimFilter(filter)
},
findOpts
);
const scimUsers = users.map( const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
({ id, externalId, username, firstName, lastName, email, isActive, createdAt, updatedAt }) => buildScimUser({
buildScimUser({ orgMembershipId: id ?? "",
orgMembershipId: id ?? "", username: externalId ?? username,
username: externalId ?? username, firstName: firstName ?? "",
firstName: firstName ?? "", lastName: lastName ?? "",
lastName: lastName ?? "", email,
email, active: isActive
active: isActive, })
createdAt,
updatedAt
})
); );
return buildScimUserList({ return buildScimUserList({
@@ -266,6 +258,11 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({ return buildScimUser({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.externalId ?? membership.username, username: membership.externalId ?? membership.username,
@@ -273,8 +270,10 @@ export const scimServiceFactory = ({
firstName: membership.firstName, firstName: membership.firstName,
lastName: membership.lastName, lastName: membership.lastName,
active: membership.isActive, active: membership.isActive,
createdAt: membership.createdAt, groups: groupMembershipsInOrg.map((group) => ({
updatedAt: membership.updatedAt value: group.groupId,
display: group.groupName
}))
}); });
}; };
@@ -323,7 +322,7 @@ export const scimServiceFactory = ({
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.NoAccess, role: OrgMembershipRole.Member,
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 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 isActive: true
}, },
@@ -350,11 +349,7 @@ export const scimServiceFactory = ({
} }
if (!user) { if (!user) {
const uniqueUsername = await normalizeUsername( const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
// external id is username
`${firstName}-${lastName}`,
userDAL
);
user = await userDAL.create( user = await userDAL.create(
{ {
username: serverCfg.trustSamlEmails ? email : uniqueUsername, username: serverCfg.trustSamlEmails ? email : uniqueUsername,
@@ -435,13 +430,10 @@ export const scimServiceFactory = ({
firstName: createdUser.firstName, firstName: createdUser.firstName,
lastName: createdUser.lastName, lastName: createdUser.lastName,
email: createdUser.email ?? "", email: createdUser.email ?? "",
active: createdOrgMembership.isActive, active: createdOrgMembership.isActive
createdAt: createdOrgMembership.createdAt,
updatedAt: createdOrgMembership.updatedAt
}); });
}; };
// partial
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => { const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
@@ -467,52 +459,37 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
const scimUser = buildScimUser({ 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({
orgMembershipId: membership.id, orgMembershipId: membership.id,
email: membership.email,
lastName: membership.lastName,
firstName: membership.firstName,
active: membership.isActive,
username: membership.externalId ?? membership.username, username: membership.externalId ?? membership.username,
createdAt: membership.createdAt, email: membership.email,
updatedAt: membership.updatedAt firstName: membership.firstName,
lastName: membership.lastName,
active
}); });
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 ({ const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
orgMembershipId,
active,
orgId,
lastName,
firstName,
email,
externalId
}: TReplaceScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
@@ -537,47 +514,26 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
const serverCfg = await getServerCfg(); await orgMembershipDAL.updateById(membership.id, {
await userDAL.transaction(async (tx) => { isActive: active
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({ return buildScimUser({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: externalId, username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName, firstName: membership.firstName,
lastName: membership.lastName, lastName: membership.lastName,
active, active,
createdAt: membership.createdAt, groups: groupMembershipsInOrg.map((group) => ({
updatedAt: membership.updatedAt value: group.groupId,
display: group.groupName
}))
}); });
}; };
@@ -614,7 +570,7 @@ export const scimServiceFactory = ({
return {}; // intentionally return empty object upon success return {}; // intentionally return empty object upon success
}; };
const listScimGroups = async ({ orgId, startIndex, limit, filter, isMembersExcluded }: TListScimGroupsDTO) => { const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.groups) if (!plan.groups)
throw new BadRequestError({ throw new BadRequestError({
@@ -647,21 +603,6 @@ export const scimServiceFactory = ({
); );
const scimGroups: TScimGroup[] = []; 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) { for await (const group of groups) {
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId); const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
@@ -671,9 +612,7 @@ export const scimServiceFactory = ({
members: members.map((member) => ({ members: members.map((member) => ({
value: member.orgMembershipId, value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}` display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
})), }))
createdAt: group.createdAt,
updatedAt: group.updatedAt
}); });
scimGroups.push(scimGroup); scimGroups.push(scimGroup);
} }
@@ -757,9 +696,7 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({ members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id, value: id,
display: `${firstName} ${lastName}` display: `${firstName} ${lastName}`
})), }))
createdAt: newGroup.group.createdAt,
updatedAt: newGroup.group.updatedAt
}); });
}; };
@@ -782,7 +719,7 @@ export const scimServiceFactory = ({
}); });
} }
const users = await groupDAL.findAllGroupPossibleMembers({ const users = await groupDAL.findAllGroupMembers({
orgId: group.orgId, orgId: group.orgId,
groupId: group.id groupId: group.id
}); });
@@ -802,17 +739,31 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({ members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id, value: id,
display: `${firstName} ${lastName}` display: `${firstName} ${lastName}`
})), }))
createdAt: group.createdAt,
updatedAt: group.updatedAt
}); });
}; };
const $replaceGroupDAL = async ( const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
groupId: string, const plan = await licenseService.getPlan(orgId);
orgId: string, if (!plan.groups)
{ displayName, members = [] }: { displayName: string; members: { value: string }[] } 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 groupDAL.transaction(async (tx) => { const updatedGroup = await groupDAL.transaction(async (tx) => {
const [group] = await groupDAL.update( const [group] = await groupDAL.update(
{ {
@@ -831,96 +782,74 @@ export const scimServiceFactory = ({
}); });
} }
const orgMemberships = members.length if (members) {
? await orgMembershipDAL.find({ const orgMemberships = await orgMembershipDAL.find({
$in: { $in: {
id: members.map((member) => member.value) id: members.map((member) => member.value)
} }
});
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const directMemberUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: false
}) })
: []; ).map((membership) => membership.userId);
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId)); const pendingGroupAdditionsUserIds = (
const userGroupMembers = await userGroupMembershipDAL.find({ await userGroupMembershipDAL.find({
groupId: group.id groupId: group.id,
}); isPending: true
const directMemberUserIds = userGroupMembers.filter((el) => !el.isPending).map((membership) => membership.userId); })
).map((pendingGroupAddition) => pendingGroupAddition.userId);
const pendingGroupAdditionsUserIds = userGroupMembers const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
.filter((el) => el.isPending) const allMembersUserIdsSet = new Set(allMembersUserIds);
.map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds); const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const allMembersUserIdsSet = new Set(allMembersUserIds); const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string)); if (toAddUserIds.length) {
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId)); await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (toAddUserIds.length) { if (toRemoveUserIds.length) {
await addUsersToGroupByUserIds({ await removeUsersFromGroupByUserIds({
group, group,
userIds: toAddUserIds.map((member) => member.userId as string), userIds: toRemoveUserIds,
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, groupProjectDAL,
groupProjectDAL, projectKeyDAL,
projectKeyDAL, tx
projectDAL, });
projectBotDAL, }
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
} }
return group; 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({ return buildScimGroup({
groupId: updatedGroup.id, groupId: updatedGroup.id,
name: updatedGroup.name, name: updatedGroup.name,
members, members
updatedAt: updatedGroup.updatedAt,
createdAt: updatedGroup.createdAt
}); });
}; };
const updateScimGroup = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => { const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.groups) if (!plan.groups)
throw new BadRequestError({ throw new BadRequestError({
@@ -942,7 +871,7 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
const group = await groupDAL.findOne({ let group = await groupDAL.findOne({
id: groupId, id: groupId,
orgId orgId
}); });
@@ -954,28 +883,64 @@ 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); const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
const scimGroup = buildScimGroup({
return buildScimGroup({
groupId: group.id, groupId: group.id,
name: group.name, name: group.name,
members: members.map((member) => ({ 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, value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}` display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
})) }))
}; });
}; };
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => { const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
@@ -1051,8 +1016,8 @@ export const scimServiceFactory = ({
createScimGroup, createScimGroup,
getScimGroup, getScimGroup,
deleteScimGroup, deleteScimGroup,
replaceScimGroup, updateScimGroupNamePut,
updateScimGroup, updateScimGroupNamePatch,
fnValidateScimToken fnValidateScimToken
}; };
}; };

View File

@@ -1,5 +1,3 @@
import { ScimPatchOperation } from "scim-patch";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
export type TCreateScimTokenDTO = { export type TCreateScimTokenDTO = {
@@ -36,25 +34,29 @@ export type TGetScimUserDTO = {
export type TCreateScimUserDTO = { export type TCreateScimUserDTO = {
externalId: string; externalId: string;
email?: string; email?: string;
firstName?: string; firstName: string;
lastName?: string; lastName: string;
orgId: string; orgId: string;
}; };
export type TUpdateScimUserDTO = { export type TUpdateScimUserDTO = {
orgMembershipId: string; orgMembershipId: string;
orgId: string; orgId: string;
operations: ScimPatchOperation[]; operations: {
op: string;
path?: string;
value?:
| string
| {
active: boolean;
};
}[];
}; };
export type TReplaceScimUserDTO = { export type TReplaceScimUserDTO = {
orgMembershipId: string; orgMembershipId: string;
active: boolean; active: boolean;
orgId: string; orgId: string;
email?: string;
firstName?: string;
lastName?: string;
externalId: string;
}; };
export type TDeleteScimUserDTO = { export type TDeleteScimUserDTO = {
@@ -67,7 +69,6 @@ export type TListScimGroupsDTO = {
filter?: string; filter?: string;
limit: number; limit: number;
orgId: string; orgId: string;
isMembersExcluded?: boolean;
}; };
export type TListScimGroups = { export type TListScimGroups = {
@@ -106,7 +107,31 @@ export type TUpdateScimGroupNamePutDTO = {
export type TUpdateScimGroupNamePatchDTO = { export type TUpdateScimGroupNamePatchDTO = {
groupId: string; groupId: string;
orgId: string; orgId: string;
operations: ScimPatchOperation[]; 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;
}[];
}; };
export type TDeleteScimGroupDTO = { export type TDeleteScimGroupDTO = {
@@ -135,10 +160,13 @@ export type TScimUser = {
type: string; type: string;
}[]; }[];
active: boolean; active: boolean;
groups: {
value: string;
display: string;
}[];
meta: { meta: {
resourceType: string; resourceType: string;
created: Date; location: null;
lastModified: Date;
}; };
}; };
@@ -148,11 +176,10 @@ export type TScimGroup = {
displayName: string; displayName: string;
members: { members: {
value: string; value: string;
display?: string; display: string;
}[]; }[];
meta: { meta: {
resourceType: string; resourceType: string;
created: Date; location: null;
lastModified: Date;
}; };
}; };

View File

@@ -1,62 +1,33 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies, TUsers } from "@app/db/schemas"; import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>; export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
export const secretApprovalPolicyDALFactory = (db: TDbClient) => { export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy); const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
const secretApprovalPolicyFindQuery = ( const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
tx: Knex,
filter: TFindFilter<TSecretApprovalPolicies>,
customFilter?: {
sapId?: string;
}
) =>
tx(TableName.SecretApprovalPolicy) tx(TableName.SecretApprovalPolicy)
// eslint-disable-next-line // eslint-disable-next-line
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
.where((qb) => {
if (customFilter?.sapId) {
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
}
})
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.leftJoin( .leftJoin(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership, .leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id"
)
.leftJoin<TUsers>(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.select( .select(
tx.ref("id").withSchema("secretApprovalPolicyApproverUser").as("approverUserId"), tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"), tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName")
)
.select(
tx.ref("approverGroupId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
tx.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverGroupFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverGroupLastName")
) )
.select( .select(
tx.ref("name").withSchema(TableName.Environment).as("envName"), tx.ref("name").withSchema(TableName.Environment).as("envName"),
@@ -84,31 +55,11 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "userApprovers" as const,
mapper: ({ mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
approverUserId: userId, userId: approverUserId,
approverEmail: email, email: approverEmail,
approverFirstName: firstName, firstName: approverFirstName,
approverLastName: lastName lastName: approverLastName
}) => ({
userId,
email,
firstName,
lastName
})
},
{
key: "approverGroupUserId",
label: "userApprovers" as const,
mapper: ({
approverGroupUserId: userId,
approverGroupEmail: email,
approverGroupFirstName: firstName,
approverGroupLastName: lastName
}) => ({
userId,
email,
firstName,
lastName
}) })
} }
] ]
@@ -120,15 +71,9 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
} }
}; };
const find = async ( const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
customFilter?: {
sapId?: string;
},
tx?: Knex
) => {
try { try {
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter, customFilter); const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const formatedDoc = sqlNestRelationships({ const formatedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
@@ -138,35 +83,11 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
...SecretApprovalPoliciesSchema.parse(data) ...SecretApprovalPoliciesSchema.parse(data)
}), }),
childrenMapper: [ childrenMapper: [
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId: id, approverUsername }) => ({
type: ApproverType.User,
name: approverUsername,
id
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
type: ApproverType.Group,
id
})
},
{ {
key: "approverUserId", key: "approverUserId",
label: "userApprovers" as const, label: "userApprovers" as const,
mapper: ({ approverUserId: userId }) => ({ mapper: ({ approverUserId }) => ({
userId userId: approverUserId
})
},
{
key: "approverGroupUserId",
label: "userApprovers" as const,
mapper: ({ approverGroupUserId: userId }) => ({
userId
}) })
} }
] ]

View File

@@ -3,13 +3,11 @@ import picomatch from "picomatch";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch"; import { containsGlobPatterns } from "@app/lib/picomatch";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
@@ -17,7 +15,6 @@ import {
TCreateSapDTO, TCreateSapDTO,
TDeleteSapDTO, TDeleteSapDTO,
TGetBoardSapDTO, TGetBoardSapDTO,
TGetSapByIdDTO,
TListSapDTO, TListSapDTO,
TUpdateSapDTO TUpdateSapDTO
} from "./secret-approval-policy-types"; } from "./secret-approval-policy-types";
@@ -31,7 +28,6 @@ type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -43,7 +39,6 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService, permissionService,
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
projectEnvDAL, projectEnvDAL,
userDAL,
licenseService licenseService
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({ const createSecretApprovalPolicy = async ({
@@ -59,19 +54,7 @@ export const secretApprovalPolicyServiceFactory = ({
environment, environment,
enforcementLevel enforcementLevel
}: TCreateSapDTO) => { }: TCreateSapDTO) => {
const groupApprovers = approvers if (approvals > approvers.length)
?.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
?.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
if (!groupApprovers.length && approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -108,48 +91,15 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
let userApproverIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find(
{
$in: {
username: userApproverNames
}
},
{ tx }
);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
}
await secretApprovalPolicyApproverDAL.insertMany( await secretApprovalPolicyApproverDAL.insertMany(
userApproverIds.map((approverUserId) => ({ approvers.map((approverUserId) => ({
approverUserId, approverUserId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
return doc; return doc;
}); });
return { ...secretApproval, environment: env, projectId }; return { ...secretApproval, environment: env, projectId };
}; };
@@ -165,18 +115,6 @@ export const secretApprovalPolicyServiceFactory = ({
secretPolicyId, secretPolicyId,
enforcementLevel enforcementLevel
}: TUpdateSapDTO) => { }: TUpdateSapDTO) => {
const groupApprovers = approvers
?.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id);
const userApprovers = approvers
?.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.name : undefined))
.filter(Boolean) as string[];
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
@@ -208,52 +146,16 @@ export const secretApprovalPolicyServiceFactory = ({
}, },
tx tx
); );
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (approvers) { if (approvers) {
let userApproverIds = userApprovers; await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (userApproverNames) {
const approverUsers = await userDAL.find(
{
$in: {
username: userApproverNames
}
},
{ tx }
);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames?.filter((username) => !approverNamesFromDb.includes(username));
if (invalidUsernames?.length) {
throw new BadRequestError({
message: `Invalid approver user: ${invalidUsernames.join(", ")}`
});
}
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
}
await secretApprovalPolicyApproverDAL.insertMany( await secretApprovalPolicyApproverDAL.insertMany(
userApproverIds.map((approverUserId) => ({ approvers.map((approverUserId) => ({
approverUserId, approverUserId,
policyId: doc.id policyId: doc.id
})), })),
tx tx
); );
} }
if (groupApprovers) {
await secretApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((approverGroupId) => ({
approverGroupId,
policyId: doc.id
})),
tx
);
}
return doc; return doc;
}); });
return { return {
@@ -358,41 +260,12 @@ export const secretApprovalPolicyServiceFactory = ({
return getSecretApprovalPolicy(projectId, environment, secretPath); return getSecretApprovalPolicy(projectId, environment, secretPath);
}; };
const getSecretApprovalPolicyById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
sapId
}: TGetSapByIdDTO) => {
const [sapPolicy] = await secretApprovalPolicyDAL.find({}, { sapId });
if (!sapPolicy) {
throw new NotFoundError({
message: "Cannot find secret approval policy"
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
sapPolicy.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
return sapPolicy;
};
return { return {
createSecretApprovalPolicy, createSecretApprovalPolicy,
updateSecretApprovalPolicy, updateSecretApprovalPolicy,
deleteSecretApprovalPolicy, deleteSecretApprovalPolicy,
getSecretApprovalPolicy, getSecretApprovalPolicy,
getSecretApprovalPolicyByProjectId, getSecretApprovalPolicyByProjectId,
getSecretApprovalPolicyOfFolder, getSecretApprovalPolicyOfFolder
getSecretApprovalPolicyById
}; };
}; };

View File

@@ -1,12 +1,10 @@
import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
export type TCreateSapDTO = { export type TCreateSapDTO = {
approvals: number; approvals: number;
secretPath?: string | null; secretPath?: string | null;
environment: string; environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approvers: string[];
projectId: string; projectId: string;
name: string; name: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
@@ -16,7 +14,7 @@ export type TUpdateSapDTO = {
secretPolicyId: string; secretPolicyId: string;
approvals?: number; approvals?: number;
secretPath?: string | null; secretPath?: string | null;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; approvers: string[];
name?: string; name?: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -27,8 +25,6 @@ export type TDeleteSapDTO = {
export type TListSapDTO = TProjectPermission; export type TListSapDTO = TProjectPermission;
export type TGetSapByIdDTO = Omit<TProjectPermission, "projectId"> & { sapId: string };
export type TGetBoardSapDTO = { export type TGetBoardSapDTO = {
projectId: string; projectId: string;
environment: string; environment: string;

View File

@@ -48,26 +48,16 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
) )
.leftJoin( .join(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin<TUsers>( .join<TUsers>(
db(TableName.Users).as("secretApprovalPolicyApproverUser"), db(TableName.Users).as("secretApprovalPolicyApproverUser"),
`${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.SecretApprovalPolicyApprover}.approverUserId`,
"secretApprovalPolicyApproverUser.id" "secretApprovalPolicyApproverUser.id"
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("secretApprovalPolicyGroupApproverUser"),
`${TableName.UserGroupMembership}.userId`,
`secretApprovalPolicyGroupApproverUser.id`
)
.leftJoin( .leftJoin(
TableName.SecretApprovalRequestReviewer, TableName.SecretApprovalRequestReviewer,
`${TableName.SecretApprovalRequest}.id`, `${TableName.SecretApprovalRequest}.id`,
@@ -81,15 +71,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"), tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("firstName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupFirstName"),
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"), tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("lastName").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupLastName"),
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"), tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"), tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"), tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
@@ -167,30 +152,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ mapper: ({
approverUserId: userId, approverUserId,
approverEmail: email, approverEmail: email,
approverUsername: username, approverUsername: username,
approverLastName: lastName, approverLastName: lastName,
approverFirstName: firstName approverFirstName: firstName
}) => ({ }) => ({
userId, userId: approverUserId,
email,
firstName,
lastName,
username
})
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({
approverGroupUserId: userId,
approverGroupEmail: email,
approverGroupUsername: username,
approverGroupLastName: lastName,
approverGroupFirstName: firstName
}) => ({
userId,
email, email,
firstName, firstName,
lastName, lastName,
@@ -268,16 +236,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.leftJoin( .join(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -306,7 +269,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
void bd void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
) )
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
@@ -327,7 +289,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
db.ref("email").withSchema("committerUser").as("committerUserEmail"), db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"), db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
@@ -373,7 +334,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => ({ userId: approverUserId }) mapper: ({ approverUserId }) => approverUserId
}, },
{ {
key: "commitId", key: "commitId",
@@ -383,11 +344,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id, id,
secretId secretId
}) })
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => ({ userId: approverGroupUserId })
} }
] ]
}); });
@@ -415,16 +371,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id` `${TableName.SecretApprovalPolicy}.id`
) )
.leftJoin( .join(
TableName.SecretApprovalPolicyApprover, TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.leftJoin(
TableName.UserGroupMembership,
`${TableName.SecretApprovalPolicyApprover}.approverGroupId`,
`${TableName.UserGroupMembership}.groupId`
)
.join<TUsers>( .join<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
@@ -453,7 +404,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
void bd void bd
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
.orWhere(`${TableName.UserGroupMembership}.userId`, userId)
) )
.select(selectAllTableCols(TableName.SecretApprovalRequest)) .select(selectAllTableCols(TableName.SecretApprovalRequest))
.select( .select(
@@ -474,7 +424,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"),
db.ref("email").withSchema("committerUser").as("committerUserEmail"), db.ref("email").withSchema("committerUser").as("committerUserEmail"),
db.ref("username").withSchema("committerUser").as("committerUserUsername"), db.ref("username").withSchema("committerUser").as("committerUserUsername"),
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
@@ -520,7 +469,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
{ {
key: "approverUserId", key: "approverUserId",
label: "approvers" as const, label: "approvers" as const,
mapper: ({ approverUserId }) => ({ userId: approverUserId }) mapper: ({ approverUserId }) => approverUserId
}, },
{ {
key: "commitId", key: "commitId",
@@ -530,13 +479,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id, id,
secretId secretId
}) })
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => ({
userId: approverGroupUserId
})
} }
] ]
}); });

View File

@@ -47,9 +47,6 @@ import {
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -92,7 +89,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">; userDAL: Pick<TUserDALFactory, "find" | "findOne">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick< projectDAL: Pick<
TProjectDALFactory, TProjectDALFactory,
@@ -107,7 +104,6 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">; secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -136,8 +132,7 @@ export const secretApprovalRequestServiceFactory = ({
secretV2BridgeDAL, secretV2BridgeDAL,
secretVersionV2BridgeDAL, secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
licenseService, licenseService
projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@@ -447,8 +442,8 @@ export const secretApprovalRequestServiceFactory = ({
); );
const hasMinApproval = const hasMinApproval =
secretApprovalRequest.policy.approvals <= secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(({ userId: approverId }) => secretApprovalRequest.policy.approvers.filter(
approverId ? reviewers[approverId] === ApprovalStatus.APPROVED : false ({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length; ).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
@@ -805,7 +800,7 @@ export const secretApprovalRequestServiceFactory = ({
const requestedByUser = await userDAL.findOne({ id: actorId }); const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
$in: { $in: {
id: policy.approvers.map((approver: { userId: string | null | undefined }) => approver.userId!) id: policy.approvers.map((approver: { userId: string }) => approver.userId)
} }
}); });
@@ -1074,25 +1069,6 @@ export const secretApprovalRequestServiceFactory = ({
return { ...doc, commits: approvalCommits }; return { ...doc, commits: approvalCommits };
}); });
const env = await projectEnvDAL.findOne({ id: policy.envId });
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
await triggerSlackNotification({
projectId,
projectDAL,
kmsService,
projectSlackConfigDAL,
notification: {
type: SlackTriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id
}
}
});
await sendApprovalEmailsFn({ await sendApprovalEmailsFn({
projectDAL, projectDAL,
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
@@ -1355,25 +1331,6 @@ export const secretApprovalRequestServiceFactory = ({
return { ...doc, commits: approvalCommits }; return { ...doc, commits: approvalCommits };
}); });
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
const env = await projectEnvDAL.findOne({ id: policy.envId });
await triggerSlackNotification({
projectId,
projectDAL,
kmsService,
projectSlackConfigDAL,
notification: {
type: SlackTriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id
}
}
});
await sendApprovalEmailsFn({ await sendApprovalEmailsFn({
projectDAL, projectDAL,
secretApprovalPolicyDAL, secretApprovalPolicyDAL,

View File

@@ -16,7 +16,6 @@ import {
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSnapshotDALFactory = ReturnType<typeof snapshotDALFactory>; export type TSnapshotDALFactory = ReturnType<typeof snapshotDALFactory>;
@@ -600,7 +599,6 @@ export const snapshotDALFactory = (db: TDbClient) => {
const pruneExcessSnapshots = async () => { const pruneExcessSnapshots = async () => {
const PRUNE_FOLDER_BATCH_SIZE = 10000; const PRUNE_FOLDER_BATCH_SIZE = 10000;
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots started`);
try { try {
let uuidOffset = "00000000-0000-0000-0000-000000000000"; let uuidOffset = "00000000-0000-0000-0000-000000000000";
// cleanup snapshots from current folders // cleanup snapshots from current folders
@@ -716,7 +714,6 @@ export const snapshotDALFactory = (db: TDbClient) => {
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "SnapshotPrune" }); throw new DatabaseError({ error, name: "SnapshotPrune" });
} }
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots completed`);
}; };
// special query for migration for secret v2 // special query for migration for secret v2

View File

@@ -1,3 +1,12 @@
export const DEFAULT_REQUEST_SCHEMA = {
// Add more default attributes here if needed
security: [
{
bearerAuth: []
}
]
};
export const GROUPS = { export const GROUPS = {
CREATE: { CREATE: {
name: "The name of the group to create.", name: "The name of the group to create.",
@@ -5,30 +14,26 @@ export const GROUPS = {
role: "The role of the group to create." role: "The role of the group to create."
}, },
UPDATE: { UPDATE: {
id: "The id of the group to update", currentSlug: "The current slug of the group to update.",
name: "The new name of the group to update to.", name: "The new name of the group to update to.",
slug: "The new slug of the group to update to.", slug: "The new slug of the group to update to.",
role: "The new role of the group to update to." role: "The new role of the group to update to."
}, },
DELETE: { DELETE: {
id: "The id of the group to delete",
slug: "The slug of the group to delete" slug: "The slug of the group to delete"
}, },
LIST_USERS: { LIST_USERS: {
id: "The id of the group to list users for", slug: "The slug of the group to list users for",
offset: "The offset to start from. If you enter 10, it will start from the 10th user.", offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
limit: "The number of users to return.", limit: "The number of users to return.",
username: "The username to search for." username: "The username to search for."
}, },
ADD_USER: { ADD_USER: {
id: "The id of the group to add the user to.", slug: "The slug of the group to add the user to.",
username: "The username of the user to add to the group." username: "The username of the user to add to the group."
}, },
GET_BY_ID: {
id: "The id of the group to fetch"
},
DELETE_USER: { DELETE_USER: {
id: "The id of the group to remove the user from.", slug: "The slug of the group to remove the user from.",
username: "The username of the user to remove from the group." username: "The username of the user to remove from the group."
} }
} as const; } as const;
@@ -367,12 +372,7 @@ export const ORGANIZATIONS = {
membershipId: "The ID of the membership to delete." membershipId: "The ID of the membership to delete."
}, },
LIST_IDENTITY_MEMBERSHIPS: { LIST_IDENTITY_MEMBERSHIPS: {
orgId: "The ID of the organization to get identity memberships from.", orgId: "The ID of the organization to get identity memberships from."
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
orderBy: "The column to order identity memberships by.",
orderDirection: "The direction identity memberships will be sorted in.",
search: "The text string that identity membership names will be filtered by."
}, },
GET_PROJECTS: { GET_PROJECTS: {
organizationId: "The ID of the organization to get projects from." organizationId: "The ID of the organization to get projects from."
@@ -413,21 +413,21 @@ export const PROJECTS = {
secretSnapshotId: "The ID of the snapshot to rollback to." secretSnapshotId: "The ID of the snapshot to rollback to."
}, },
ADD_GROUP_TO_PROJECT: { ADD_GROUP_TO_PROJECT: {
projectId: "The ID of the project to add the group to.", projectSlug: "The slug of the project to add the group to.",
groupId: "The ID of the group to add to the project.", groupSlug: "The slug of the group to add to the project.",
role: "The role for the group to assume in the project." role: "The role for the group to assume in the project."
}, },
UPDATE_GROUP_IN_PROJECT: { UPDATE_GROUP_IN_PROJECT: {
projectId: "The ID of the project to update the group in.", projectSlug: "The slug of the project to update the group in.",
groupId: "The ID of the group to update in the project.", groupSlug: "The slug of the group to update in the project.",
roles: "A list of roles to update the group to." roles: "A list of roles to update the group to."
}, },
REMOVE_GROUP_FROM_PROJECT: { REMOVE_GROUP_FROM_PROJECT: {
projectId: "The ID of the project to delete the group from.", projectSlug: "The slug of the project to delete the group from.",
groupId: "The ID of the group to delete from the project." groupSlug: "The slug of the group to delete from the project."
}, },
LIST_GROUPS_IN_PROJECT: { LIST_GROUPS_IN_PROJECT: {
projectId: "The ID of the project to list groups for." projectSlug: "The slug of the project to list groups for."
}, },
LIST_INTEGRATION: { LIST_INTEGRATION: {
workspaceId: "The ID of the project to list integrations for." workspaceId: "The ID of the project to list integrations for."
@@ -456,9 +456,7 @@ export const PROJECT_USERS = {
INVITE_MEMBER: { INVITE_MEMBER: {
projectId: "The ID of the project to invite the member to.", projectId: "The ID of the project to invite the member to.",
emails: "A list of organization member emails to invite to the project.", emails: "A list of organization member emails to invite to the project.",
usernames: "A list of usernames to invite to the project.", usernames: "A list of usernames to invite to the project."
roleSlugs:
"A list of role slugs to assign to the newly created project membership. If nothing is provided, it will default to the Member role."
}, },
REMOVE_MEMBER: { REMOVE_MEMBER: {
projectId: "The ID of the project to remove the member from.", projectId: "The ID of the project to remove the member from.",
@@ -481,12 +479,7 @@ export const PROJECT_USERS = {
export const PROJECT_IDENTITIES = { export const PROJECT_IDENTITIES = {
LIST_IDENTITY_MEMBERSHIPS: { LIST_IDENTITY_MEMBERSHIPS: {
projectId: "The ID of the project to get identity memberships from.", projectId: "The ID of the project to get identity memberships from."
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
orderBy: "The column to order identity memberships by.",
orderDirection: "The direction identity memberships will be sorted in.",
search: "The text string that identity membership names will be filtered by."
}, },
GET_IDENTITY_MEMBERSHIP_BY_ID: { GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the identity to get the membership for.", identityId: "The ID of the identity to get the membership for.",
@@ -701,46 +694,11 @@ export const SECRET_IMPORTS = {
} }
} as const; } as const;
export const DASHBOARD = {
SECRET_OVERVIEW_LIST: {
projectId: "The ID of the project to list secrets/folders from.",
environments:
"The slugs of the environments to list secrets/folders from (comma separated, ie 'environments=dev,staging,prod').",
secretPath: "The secret path to list secrets/folders from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
limit: "The number of secrets/folders to return.",
orderBy: "The column to order secrets/folders by.",
orderDirection: "The direction to order secrets/folders in.",
search: "The text string to filter secret keys and folder names by.",
includeSecrets: "Whether to include project secrets in the response.",
includeFolders: "Whether to include project folders in the response.",
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
},
SECRET_DETAILS_LIST: {
projectId: "The ID of the project to list secrets/folders from.",
environment: "The slug of the environment to list secrets/folders from.",
secretPath: "The secret path to list secrets/folders from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th secret/folder.",
limit: "The number of secrets/folders to return.",
orderBy: "The column to order secrets/folders by.",
orderDirection: "The direction to order secrets/folders in.",
search: "The text string to filter secret keys and folder names by.",
tags: "The tags to filter secrets by (comma separated, ie 'tags=billing,engineering').",
includeSecrets: "Whether to include project secrets in the response.",
includeFolders: "Whether to include project folders in the response.",
includeImports: "Whether to include project secret imports in the response.",
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
}
} as const;
export const AUDIT_LOGS = { export const AUDIT_LOGS = {
EXPORT: { EXPORT: {
projectId: workspaceId: "The ID of the project to export audit logs from.",
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
eventType: "The type of the event to export.", eventType: "The type of the event to export.",
userAgentType: "Choose which consuming application to export audit logs for.", userAgentType: "Choose which consuming application to export audit logs for.",
eventMetadata:
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
startDate: "The date to start the export from.", startDate: "The date to start the export from.",
endDate: "The date to end the export at.", endDate: "The date to end the export at.",
offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.", offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
@@ -1015,10 +973,6 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS 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.", kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.", 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'.", shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
@@ -1088,18 +1042,14 @@ export const CERTIFICATE_AUTHORITIES = {
maxPathLength: maxPathLength:
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.", "The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
keyAlgorithm: keyAlgorithm:
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA.", "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA."
requireTemplateForIssuance:
"Whether or not certificates for this CA can only be issued through certificate templates."
}, },
GET: { GET: {
caId: "The ID of the CA to get" caId: "The ID of the CA to get"
}, },
UPDATE: { UPDATE: {
caId: "The ID of the CA to update", caId: "The ID of the CA to update",
status: "The status of the CA to update to. This can be one of active or disabled", status: "The status of the CA to update to. This can be one of active or disabled"
requireTemplateForIssuance:
"Whether or not certificates for this CA can only be issued through certificate templates."
}, },
DELETE: { DELETE: {
caId: "The ID of the CA to delete" caId: "The ID of the CA to delete"
@@ -1122,10 +1072,6 @@ export const CERTIFICATE_AUTHORITIES = {
certificateChain: "The certificate chain of the CA", certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the CA certificate" serialNumber: "The serial number of the CA certificate"
}, },
GET_CERT_BY_ID: {
caId: "The ID of the CA to get the CA certificate from",
caCertId: "The ID of the CA certificate to get"
},
GET_CA_CERTS: { GET_CA_CERTS: {
caId: "The ID of the CA to get the CA certificates for", caId: "The ID of the CA to get the CA certificates for",
certificate: "The certificate body of the CA certificate", certificate: "The certificate body of the CA certificate",
@@ -1165,15 +1111,11 @@ export const CERTIFICATE_AUTHORITIES = {
issuingCaCertificate: "The certificate of the issuing CA", issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate", certificateChain: "The certificate chain of the issued certificate",
privateKey: "The private key of the issued certificate", privateKey: "The private key of the issued certificate",
serialNumber: "The serial number of the issued certificate", serialNumber: "The serial number of the issued certificate"
keyUsages: "The key usage extension of the certificate",
extendedKeyUsages: "The extended key usage extension of the certificate"
}, },
SIGN_CERT: { SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from", caId: "The ID of the CA to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to", pkiCollectionId: "The ID of the PKI collection to add the certificate to",
keyUsages: "The key usage extension of the certificate",
extendedKeyUsages: "The extended key usage extension of the certificate",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate", friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate", commonName: "The common name (CN) for the certificate",
@@ -1223,10 +1165,7 @@ export const CERTIFICATE_TEMPLATES = {
name: "The name of the template", name: "The name of the template",
commonName: "The regular expression string to use for validating common names", commonName: "The regular expression string to use for validating common names",
subjectAlternativeName: "The regular expression string to use for validating subject alternative names", subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
ttl: "The max TTL for the template", ttl: "The max TTL for the template"
keyUsages: "The key usage constraint or default value for when template is used during certificate issuance",
extendedKeyUsages:
"The extended key usage constraint or default value for when template is used during certificate issuance"
}, },
GET: { GET: {
certificateTemplateId: "The ID of the certificate template to get" certificateTemplateId: "The ID of the certificate template to get"
@@ -1238,11 +1177,7 @@ export const CERTIFICATE_TEMPLATES = {
name: "The updated name of the template", name: "The updated name of the template",
commonName: "The updated regular expression string for validating common names", commonName: "The updated regular expression string for validating common names",
subjectAlternativeName: "The updated regular expression string for validating subject alternative names", subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
ttl: "The updated max TTL for the template", ttl: "The updated max TTL for the template"
keyUsages:
"The updated key usage constraint or default value for when template is used during certificate issuance",
extendedKeyUsages:
"The updated extended key usage constraint or default value for when template is used during certificate issuance"
}, },
DELETE: { DELETE: {
certificateTemplateId: "The ID of the certificate template to delete" certificateTemplateId: "The ID of the certificate template to delete"

View File

@@ -1,57 +0,0 @@
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);
}
};
};

View File

@@ -146,9 +146,7 @@ const envSchema = z
PLAIN_API_KEY: zpStr(z.string().optional()), PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()), PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert")
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
}) })
.transform((data) => ({ .transform((data) => ({
...data, ...data,

View File

@@ -5,7 +5,6 @@ import nacl from "tweetnacl";
import tweetnacl from "tweetnacl-util"; import tweetnacl from "tweetnacl-util";
import { TUserEncryptionKeys } from "@app/db/schemas"; import { TUserEncryptionKeys } from "@app/db/schemas";
import { UserEncryption } from "@app/services/user/user-types";
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption"; import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
@@ -37,16 +36,12 @@ export const srpCheckClientProof = async (
// Ghost user related: // Ghost user related:
// This functionality is intended for ghost user logic. This happens on the frontend when a user is being created. // This functionality is intended for ghost user logic. This happens on the frontend when a user is being created.
// We replicate the same functionality on the backend when creating a ghost user. // We replicate the same functionality on the backend when creating a ghost user.
export const generateUserSrpKeys = async ( export const generateUserSrpKeys = async (email: string, password: string) => {
email: string,
password: string,
customKeys?: { publicKey: string; privateKey: string }
) => {
const pair = nacl.box.keyPair(); const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey; const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey; const publicKeyUint8Array = pair.publicKey;
const privateKey = customKeys?.privateKey || tweetnacl.encodeBase64(secretKeyUint8Array); const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array);
const publicKey = customKeys?.publicKey || tweetnacl.encodeBase64(publicKeyUint8Array); const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array);
// eslint-disable-next-line // eslint-disable-next-line
const client = new jsrp.client(); const client = new jsrp.client();
@@ -116,7 +111,7 @@ export const getUserPrivateKey = async (
| "encryptionVersion" | "encryptionVersion"
> >
) => { ) => {
if (user.encryptionVersion === UserEncryption.V1) { if (user.encryptionVersion === 1) {
return decryptSymmetric128BitHexKeyUTF8({ return decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey, ciphertext: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
@@ -124,12 +119,7 @@ export const getUserPrivateKey = async (
key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0") key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
}); });
} }
if ( if (user.encryptionVersion === 2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag) {
user.encryptionVersion === UserEncryption.V2 &&
user.protectedKey &&
user.protectedKeyIV &&
user.protectedKeyTag
) {
const derivedKey = await argon2.hash(password, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,

View File

@@ -9,8 +9,3 @@ export const removeTrailingSlash = (str: string) => {
return str.endsWith("/") ? str.slice(0, -1) : str; return str.endsWith("/") ? str.slice(0, -1) : str;
}; };
export const prefixWithSlash = (str: string) => {
if (str.startsWith("/")) return str;
return `/${str}`;
};

View File

@@ -51,17 +51,11 @@ export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean
: unknown) : unknown)
>; >;
export type TFindOpt< export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
R extends object = object,
TCount extends boolean = boolean,
TCountDistinct extends keyof R | undefined = undefined
> = {
limit?: number; limit?: number;
offset?: number; offset?: number;
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>; sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
groupBy?: keyof R;
count?: TCount; count?: TCount;
countDistinct?: TCountDistinct;
tx?: Knex; tx?: Knex;
}; };
@@ -92,18 +86,13 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Find one" }); throw new DatabaseError({ error, name: "Find one" });
} }
}, },
find: async < find: async <TCount extends boolean = false>(
TCount extends boolean = false,
TCountDistinct extends keyof Tables[Tname]["base"] | undefined = undefined
>(
filter: TFindFilter<Tables[Tname]["base"]>, filter: TFindFilter<Tables[Tname]["base"]>,
{ offset, limit, sort, count, tx, countDistinct }: TFindOpt<Tables[Tname]["base"], TCount, TCountDistinct> = {} { offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
) => { ) => {
try { try {
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter)); const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
if (countDistinct) { if (count) {
void query.countDistinct(countDistinct);
} else if (count) {
void query.select(db.raw("COUNT(*) OVER() AS count")); void query.select(db.raw("COUNT(*) OVER() AS count"));
void query.select("*"); void query.select("*");
} }
@@ -112,8 +101,7 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
if (sort) { if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
} }
const res = (await query) as TFindReturn<typeof query, TCount>;
const res = (await query) as TFindReturn<typeof query, TCountDistinct extends undefined ? TCount : true>;
return res; return res;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find one" }); throw new DatabaseError({ error, name: "Find one" });

View File

@@ -1,121 +0,0 @@
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;
}
}
};

View File

@@ -52,8 +52,3 @@ export enum SecretSharingAccessType {
Anyone = "anyone", Anyone = "anyone",
Organization = "organization" Organization = "organization"
} }
export enum OrderByDirection {
ASC = "asc",
DESC = "desc"
}

View File

@@ -1,2 +1,2 @@
export { isDisposableEmail } from "./validate-email"; export { isDisposableEmail } from "./validate-email";
export { blockLocalAndPrivateIpAddresses } from "./validate-url"; export { validateLocalIps } from "./validate-url";

View File

@@ -1,16 +1,10 @@
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
export const isDisposableEmail = async (emails: string | string[]) => { export const isDisposableEmail = async (email: string) => {
const emailDomain = email.split("@")[1];
const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8"); const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8");
if (Array.isArray(emails)) {
return emails.some((email) => {
const emailDomain = email.split("@")[1];
return disposableEmails.split("\n").includes(emailDomain);
});
}
const emailDomain = emails.split("@")[1];
if (disposableEmails.split("\n").includes(emailDomain)) return true; if (disposableEmails.split("\n").includes(emailDomain)) return true;
return false; return false;
}; };

View File

@@ -1,7 +1,7 @@
import { getConfig } from "../config/env"; import { getConfig } from "../config/env";
import { BadRequestError } from "../errors"; import { BadRequestError } from "../errors";
export const blockLocalAndPrivateIpAddresses = (url: string) => { export const validateLocalIps = (url: string) => {
const validUrl = new URL(url); const validUrl = new URL(url);
const appCfg = getConfig(); const appCfg = getConfig();
// on cloud local ips are not allowed // on cloud local ips are not allowed

View File

@@ -7,11 +7,7 @@ import {
TScanFullRepoEventPayload, TScanFullRepoEventPayload,
TScanPushEventPayload TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { import { TSyncSecretsDTO } from "@app/services/secret/secret-types";
TFailedIntegrationSyncEmailsPayload,
TIntegrationSyncPayload,
TSyncSecretsDTO
} from "@app/services/secret/secret-types";
export enum QueueName { export enum QueueName {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@@ -46,7 +42,6 @@ export enum QueueJobs {
SecWebhook = "secret-webhook-trigger", SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats", TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull", IntegrationSync = "secret-integration-pull",
SendFailedIntegrationSyncEmails = "send-failed-integration-sync-emails",
SecretScan = "secret-scan", SecretScan = "secret-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost-job", UpgradeProjectToGhost = "upgrade-project-to-ghost-job",
DynamicSecretRevocation = "dynamic-secret-revocation", DynamicSecretRevocation = "dynamic-secret-revocation",
@@ -93,26 +88,16 @@ export type TQueueJobTypes = {
name: QueueJobs.SecWebhook; name: QueueJobs.SecWebhook;
payload: { projectId: string; environment: string; secretPath: string; depth?: number }; payload: { projectId: string; environment: string; secretPath: string; depth?: number };
}; };
[QueueName.IntegrationSync]: {
[QueueName.AccessTokenStatusUpdate]: name: QueueJobs.IntegrationSync;
| { payload: {
name: QueueJobs.IdentityAccessTokenStatusUpdate; projectId: string;
payload: { identityAccessTokenId: string; numberOfUses: number }; environment: string;
} secretPath: string;
| { depth?: number;
name: QueueJobs.ServiceTokenStatusUpdate; deDupeQueue?: Record<string, boolean>;
payload: { serviceTokenId: string }; };
}; };
[QueueName.IntegrationSync]:
| {
name: QueueJobs.IntegrationSync;
payload: TIntegrationSyncPayload;
}
| {
name: QueueJobs.SendFailedIntegrationSyncEmails;
payload: TFailedIntegrationSyncEmailsPayload;
};
[QueueName.SecretFullRepoScan]: { [QueueName.SecretFullRepoScan]: {
name: QueueJobs.SecretScan; name: QueueJobs.SecretScan;
payload: TScanFullRepoEventPayload; payload: TScanFullRepoEventPayload;
@@ -166,6 +151,15 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration; name: QueueJobs.ProjectV3Migration;
payload: { projectId: string }; payload: { projectId: string };
}; };
[QueueName.AccessTokenStatusUpdate]:
| {
name: QueueJobs.IdentityAccessTokenStatusUpdate;
payload: { identityAccessTokenId: string; numberOfUses: number };
}
| {
name: QueueJobs.ServiceTokenStatusUpdate;
payload: { serviceTokenId: string };
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -10,7 +10,7 @@ import fastifyFormBody from "@fastify/formbody";
import helmet from "@fastify/helmet"; import helmet from "@fastify/helmet";
import type { FastifyRateLimitOptions } from "@fastify/rate-limit"; import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
import ratelimiter from "@fastify/rate-limit"; import ratelimiter from "@fastify/rate-limit";
import fastify from "fastify"; import fasitfy from "fastify";
import { Knex } from "knex"; import { Knex } from "knex";
import { Logger } from "pino"; import { Logger } from "pino";
@@ -39,7 +39,7 @@ type TMain = {
// Run the server! // Run the server!
export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => { export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
const appCfg = getConfig(); const appCfg = getConfig();
const server = fastify({ const server = fasitfy({
logger: appCfg.NODE_ENV === "test" ? false : logger, logger: appCfg.NODE_ENV === "test" ? false : logger,
trustProxy: true, trustProxy: true,
connectionTimeout: 30 * 1000, connectionTimeout: 30 * 1000,
@@ -49,21 +49,6 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
server.setValidatorCompiler(validatorCompiler); server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler); 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 { try {
await server.register<FastifyCookieOptions>(cookie, { await server.register<FastifyCookieOptions>(cookie, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY secret: appCfg.COOKIE_SECRET_SIGN_KEY

Some files were not shown because too many files have changed in this diff Show More