diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 000000000..bce047f77 --- /dev/null +++ b/.env.test.example @@ -0,0 +1,4 @@ +REDIS_URL=redis://localhost:6379 +DB_CONNECTION_URI=postgres://infisical:infisical@localhost/infisical?sslmode=disable +AUTH_SECRET=4bnfe4e407b8921c104518903515b218 +ENCRYPTION_KEY=4bnfe4e407b8921c104518903515b218 \ No newline at end of file diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index 39c9c6fb7..7fba6c321 100644 --- a/.github/workflows/check-api-for-breaking-changes.yml +++ b/.github/workflows/check-api-for-breaking-changes.yml @@ -28,7 +28,7 @@ jobs: run: docker build --tag infisical-api . working-directory: backend - name: Start postgres and redis - run: touch .env && docker-compose -f docker-compose.pg.yml up -d db redis + run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis - name: Start the server run: | echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env @@ -70,6 +70,6 @@ jobs: run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR - name: cleanup run: | - docker-compose -f "docker-compose.pg.yml" down + docker-compose -f "docker-compose.dev.yml" down docker stop infisical-api - docker remove infisical-api + docker remove infisical-api \ No newline at end of file diff --git a/.github/workflows/check-fe-pull-request.yml b/.github/workflows/check-fe-ts-and-lint.yml similarity index 67% rename from .github/workflows/check-fe-pull-request.yml rename to .github/workflows/check-fe-ts-and-lint.yml index 75465a014..17e5a9d74 100644 --- a/.github/workflows/check-fe-pull-request.yml +++ b/.github/workflows/check-fe-ts-and-lint.yml @@ -1,4 +1,4 @@ -name: Check Frontend Pull Request +name: Check Frontend Type and Lint check on: pull_request: @@ -10,8 +10,8 @@ on: - "frontend/.eslintrc.js" jobs: - check-fe-pr: - name: Check + check-fe-ts-lint: + name: Check Frontend Type and Lint check runs-on: ubuntu-latest timeout-minutes: 15 @@ -25,12 +25,11 @@ jobs: cache: "npm" cache-dependency-path: frontend/package-lock.json - name: ๐Ÿ“ฆ Install dependencies - run: npm ci --only-production --ignore-scripts + run: npm install working-directory: frontend - # - - # name: ๐Ÿงช Run tests - # run: npm run test:ci - # working-directory: frontend - - name: ๐Ÿ—๏ธ Run build - run: npm run build + - name: ๐Ÿ—๏ธ Run Type check + run: npm run type:check + working-directory: frontend + - name: ๐Ÿ—๏ธ Run Link check + run: npm run lint:fix working-directory: frontend diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index 54f4f4fbe..f08e882aa 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -5,9 +5,14 @@ on: - "infisical/v*.*.*-postgres" 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-standalone: name: Build infisical standalone image postgres runs-on: ubuntu-latest + needs: [infisical-tests] steps: - name: Extract version from tag id: extract_version diff --git a/.github/workflows/run-backend-tests.yml b/.github/workflows/run-backend-tests.yml new file mode 100644 index 000000000..edb58f9a6 --- /dev/null +++ b/.github/workflows/run-backend-tests.yml @@ -0,0 +1,47 @@ +name: "Run backend tests" + +on: + pull_request: + types: [opened, synchronize] + paths: + - "backend/**" + - "!backend/README.md" + - "!backend/.*" + - "backend/.eslintrc.js" + workflow_call: + +jobs: + check-be-pr: + name: Run integration test + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - uses: KengoTODA/actions-setup-docker-compose@v1 + if: ${{ env.ACT }} + name: Install `docker-compose` for local simulations + with: + version: "2.14.2" + - name: ๐Ÿ”ง Setup Node 20 + uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: backend/package-lock.json + - name: Install dependencies + run: npm install + working-directory: backend + - name: Start postgres and redis + run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis + - name: Start integration test + run: npm run test:e2e + working-directory: backend + env: + REDIS_URL: redis://172.17.0.1:6379 + DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable + AUTH_SECRET: something-random + ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218 + - name: cleanup + run: | + docker-compose -f "docker-compose.dev.yml" down \ No newline at end of file diff --git a/backend/.eslintignore b/backend/.eslintignore index c767a4a9e..660e6d10f 100644 --- a/backend/.eslintignore +++ b/backend/.eslintignore @@ -1,2 +1,3 @@ vitest-environment-infisical.ts vitest.config.ts +vitest.e2e.config.ts diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index e99c48c09..9c558919b 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -21,6 +21,18 @@ module.exports = { tsconfigRootDir: __dirname }, root: true, + overrides: [ + { + files: ["./e2e-test/**/*"], + rules: { + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-call": "off", + } + } + ], rules: { "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-unsafe-enum-comparison": "off", diff --git a/backend/e2e-test/routes/v1/identity.spec.ts b/backend/e2e-test/routes/v1/identity.spec.ts new file mode 100644 index 000000000..ccb530c79 --- /dev/null +++ b/backend/e2e-test/routes/v1/identity.spec.ts @@ -0,0 +1,71 @@ +import { OrgMembershipRole } from "@app/db/schemas"; +import { seedData1 } from "@app/db/seed-data"; + +export const createIdentity = async (name: string, role: string) => { + const createIdentityRes = await testServer.inject({ + method: "POST", + url: "/api/v1/identities", + body: { + name, + role, + organizationId: seedData1.organization.id + }, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(createIdentityRes.statusCode).toBe(200); + return createIdentityRes.json().identity; +}; + +export const deleteIdentity = async (id: string) => { + const deleteIdentityRes = await testServer.inject({ + method: "DELETE", + url: `/api/v1/identities/${id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(deleteIdentityRes.statusCode).toBe(200); + return deleteIdentityRes.json().identity; +}; + +describe("Identity v1", async () => { + test("Create identity", async () => { + const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin); + expect(newIdentity.name).toBe("mac1"); + expect(newIdentity.authMethod).toBeNull(); + + await deleteIdentity(newIdentity.id); + }); + + test("Update identity", async () => { + const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin); + expect(newIdentity.name).toBe("mac1"); + expect(newIdentity.authMethod).toBeNull(); + + const updatedIdentity = await testServer.inject({ + method: "PATCH", + url: `/api/v1/identities/${newIdentity.id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + name: "updated-mac-1", + role: OrgMembershipRole.Member + } + }); + + expect(updatedIdentity.statusCode).toBe(200); + expect(updatedIdentity.json().identity.name).toBe("updated-mac-1"); + + await deleteIdentity(newIdentity.id); + }); + + test("Delete Identity", async () => { + const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin); + + const deletedIdentity = await deleteIdentity(newIdentity.id); + expect(deletedIdentity.name).toBe("mac1"); + }); +}); diff --git a/backend/e2e-test/routes/v1/login.spec.ts b/backend/e2e-test/routes/v1/login.spec.ts index c95e1e016..cd6ec3194 100644 --- a/backend/e2e-test/routes/v1/login.spec.ts +++ b/backend/e2e-test/routes/v1/login.spec.ts @@ -1,6 +1,7 @@ -import { seedData1 } from "@app/db/seed-data"; import jsrp from "jsrp"; +import { seedData1 } from "@app/db/seed-data"; + describe("Login V1 Router", async () => { // eslint-disable-next-line const client = new jsrp.client(); diff --git a/backend/e2e-test/routes/v1/project-env.spec.ts b/backend/e2e-test/routes/v1/project-env.spec.ts index 936cfa859..ec06d6474 100644 --- a/backend/e2e-test/routes/v1/project-env.spec.ts +++ b/backend/e2e-test/routes/v1/project-env.spec.ts @@ -1,6 +1,40 @@ import { seedData1 } from "@app/db/seed-data"; import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project"; +const createProjectEnvironment = async (name: string, slug: string) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/workspace/${seedData1.project.id}/environments`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + name, + slug + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("environment"); + return payload.environment; +}; + +const deleteProjectEnvironment = async (envId: string) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v1/workspace/${seedData1.project.id}/environments/${envId}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("environment"); + return payload.environment; +}; + describe("Project Environment Router", async () => { test("Get default environments", async () => { const res = await testServer.inject({ @@ -31,24 +65,10 @@ describe("Project Environment Router", async () => { expect(payload.workspace.environments.length).toBe(3); }); - const mockProjectEnv = { name: "temp", slug: "temp", id: "" }; // id will be filled in create op + const mockProjectEnv = { name: "temp", slug: "temp" }; // id will be filled in create op test("Create environment", async () => { - const res = await testServer.inject({ - method: "POST", - url: `/api/v1/workspace/${seedData1.project.id}/environments`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - }, - body: { - name: mockProjectEnv.name, - slug: mockProjectEnv.slug - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("environment"); - expect(payload.environment).toEqual( + const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug); + expect(newEnvironment).toEqual( expect.objectContaining({ id: expect.any(String), name: mockProjectEnv.name, @@ -59,14 +79,15 @@ describe("Project Environment Router", async () => { updatedAt: expect.any(String) }) ); - mockProjectEnv.id = payload.environment.id; + await deleteProjectEnvironment(newEnvironment.id); }); test("Update environment", async () => { + const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug); const updatedName = { name: "temp#2", slug: "temp2" }; const res = await testServer.inject({ method: "PATCH", - url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`, + url: `/api/v1/workspace/${seedData1.project.id}/environments/${newEnvironment.id}`, headers: { authorization: `Bearer ${jwtAuthToken}` }, @@ -82,7 +103,7 @@ describe("Project Environment Router", async () => { expect(payload).toHaveProperty("environment"); expect(payload.environment).toEqual( expect.objectContaining({ - id: expect.any(String), + id: newEnvironment.id, name: updatedName.name, slug: updatedName.slug, projectId: seedData1.project.id, @@ -91,61 +112,21 @@ describe("Project Environment Router", async () => { updatedAt: expect.any(String) }) ); - mockProjectEnv.name = updatedName.name; - mockProjectEnv.slug = updatedName.slug; + await deleteProjectEnvironment(newEnvironment.id); }); test("Delete environment", async () => { - const res = await testServer.inject({ - method: "DELETE", - url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("environment"); - expect(payload.environment).toEqual( + const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug); + const deletedProjectEnvironment = await deleteProjectEnvironment(newEnvironment.id); + expect(deletedProjectEnvironment).toEqual( expect.objectContaining({ - id: expect.any(String), + id: deletedProjectEnvironment.id, name: mockProjectEnv.name, slug: mockProjectEnv.slug, - position: 1, + position: 4, createdAt: expect.any(String), updatedAt: expect.any(String) }) ); }); - - // after all these opreations the list of environment should be still same - test("Default list of environment", async () => { - const res = await testServer.inject({ - method: "GET", - url: `/api/v1/workspace/${seedData1.project.id}`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("workspace"); - // check for default environments - expect(payload).toEqual({ - workspace: expect.objectContaining({ - name: seedData1.project.name, - id: seedData1.project.id, - slug: seedData1.project.slug, - environments: expect.arrayContaining([ - expect.objectContaining(DEFAULT_PROJECT_ENVS[0]), - expect.objectContaining(DEFAULT_PROJECT_ENVS[1]), - expect.objectContaining(DEFAULT_PROJECT_ENVS[2]) - ]) - }) - }); - // ensure only two default environments exist - expect(payload.workspace.environments.length).toBe(3); - }); }); diff --git a/backend/e2e-test/routes/v1/secret-folder.spec.ts b/backend/e2e-test/routes/v1/secret-folder.spec.ts index bc290fed3..4d4bd7ab4 100644 --- a/backend/e2e-test/routes/v1/secret-folder.spec.ts +++ b/backend/e2e-test/routes/v1/secret-folder.spec.ts @@ -1,5 +1,40 @@ import { seedData1 } from "@app/db/seed-data"; +const createFolder = async (dto: { path: string; name: string }) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + name: dto.name, + path: dto.path + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder; +}; + +const deleteFolder = async (dto: { path: string; id: string }) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v1/folders/${dto.id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + path: dto.path + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder; +}; + describe("Secret Folder Router", async () => { test.each([ { name: "folder1", path: "/" }, // one in root @@ -7,30 +42,15 @@ describe("Secret Folder Router", async () => { { name: "folder2", path: "/" }, { name: "folder1", path: "/level1/level2" } // this should not create folder return same thing ])("Create folder $name in $path", async ({ name, path }) => { - const res = await testServer.inject({ - method: "POST", - url: `/api/v1/folders`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - }, - body: { - workspaceId: seedData1.project.id, - environment: seedData1.environment.slug, - name, - path - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("folder"); + const createdFolder = await createFolder({ path, name }); // check for default environments - expect(payload).toEqual({ - folder: expect.objectContaining({ + expect(createdFolder).toEqual( + expect.objectContaining({ name, id: expect.any(String) }) - }); + ); + await deleteFolder({ path, id: createdFolder.id }); }); test.each([ @@ -43,6 +63,8 @@ describe("Secret Folder Router", async () => { }, { path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } } ])("Get folders $path", async ({ path, expected }) => { + const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path }))); + const res = await testServer.inject({ method: "GET", url: `/api/v1/folders`, @@ -59,36 +81,22 @@ describe("Secret Folder Router", async () => { expect(res.statusCode).toBe(200); const payload = JSON.parse(res.payload); expect(payload).toHaveProperty("folders"); - expect(payload.folders.length).toBe(expected.length); - expect(payload).toEqual({ folders: expected.folders.map((el) => expect.objectContaining(el)) }); - }); - - let toBeDeleteFolderId = ""; - test("Update a deep folder", async () => { - const res = await testServer.inject({ - method: "PATCH", - url: `/api/v1/folders/folder1`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - }, - body: { - workspaceId: seedData1.project.id, - environment: seedData1.environment.slug, - name: "folder-updated", - path: "/level1/level2" - } + expect(payload.folders.length >= expected.folders.length).toBeTruthy(); + expect(payload).toEqual({ + folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el))) }); - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("folder"); - expect(payload.folder).toEqual( + await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id }))); + }); + + test("Update a deep folder", async () => { + const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" }); + expect(newFolder).toEqual( expect.objectContaining({ id: expect.any(String), name: "folder-updated" }) ); - toBeDeleteFolderId = payload.folder.id; const resUpdatedFolders = await testServer.inject({ method: "GET", @@ -106,14 +114,16 @@ describe("Secret Folder Router", async () => { expect(resUpdatedFolders.statusCode).toBe(200); const updatedFolderList = JSON.parse(resUpdatedFolders.payload); expect(updatedFolderList).toHaveProperty("folders"); - expect(updatedFolderList.folders.length).toEqual(1); expect(updatedFolderList.folders[0].name).toEqual("folder-updated"); + + await deleteFolder({ path: "/level1/level2", id: newFolder.id }); }); test("Delete a deep folder", async () => { + const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" }); const res = await testServer.inject({ method: "DELETE", - url: `/api/v1/folders/${toBeDeleteFolderId}`, + url: `/api/v1/folders/${newFolder.id}`, headers: { authorization: `Bearer ${jwtAuthToken}` }, diff --git a/backend/e2e-test/routes/v1/secret-import.spec.ts b/backend/e2e-test/routes/v1/secret-import.spec.ts index f42c033c2..ba37b5f42 100644 --- a/backend/e2e-test/routes/v1/secret-import.spec.ts +++ b/backend/e2e-test/routes/v1/secret-import.spec.ts @@ -1,32 +1,57 @@ import { seedData1 } from "@app/db/seed-data"; -describe("Secret Folder Router", async () => { +const createSecretImport = async (importPath: string, importEnv: string) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/secret-imports`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/", + import: { + environment: importEnv, + path: importPath + } + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImport"); + return payload.secretImport; +}; + +const deleteSecretImport = async (id: string) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v1/secret-imports/${id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/" + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImport"); + return payload.secretImport; +}; + +describe("Secret Import Router", async () => { test.each([ { importEnv: "dev", importPath: "/" }, // one in root { importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones ])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => { - 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"); // check for default environments - expect(payload.secretImport).toEqual( + const payload = await createSecretImport(importPath, importEnv); + expect(payload).toEqual( expect.objectContaining({ id: expect.any(String), importPath: expect.any(String), @@ -37,10 +62,12 @@ describe("Secret Folder Router", async () => { }) }) ); + await deleteSecretImport(payload.id); }); - let testSecretImportId = ""; test("Get secret imports", async () => { + const createdImport1 = await createSecretImport("/", "dev"); + const createdImport2 = await createSecretImport("/", "staging"); const res = await testServer.inject({ method: "GET", url: `/api/v1/secret-imports`, @@ -58,7 +85,6 @@ describe("Secret Folder Router", async () => { const payload = JSON.parse(res.payload); expect(payload).toHaveProperty("secretImports"); expect(payload.secretImports.length).toBe(2); - testSecretImportId = payload.secretImports[0].id; expect(payload.secretImports).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -72,12 +98,20 @@ describe("Secret Folder Router", async () => { }) ]) ); + await deleteSecretImport(createdImport1.id); + await deleteSecretImport(createdImport2.id); }); test("Update secret import position", async () => { - const res = await testServer.inject({ + const devImportDetails = { path: "/", envSlug: "dev" }; + const stagingImportDetails = { path: "/", envSlug: "staging" }; + + const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug); + const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug); + + const updateImportRes = await testServer.inject({ method: "PATCH", - url: `/api/v1/secret-imports/${testSecretImportId}`, + url: `/api/v1/secret-imports/${createdImport1.id}`, headers: { authorization: `Bearer ${jwtAuthToken}` }, @@ -91,8 +125,8 @@ describe("Secret Folder Router", async () => { } }); - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); + expect(updateImportRes.statusCode).toBe(200); + const payload = JSON.parse(updateImportRes.payload); expect(payload).toHaveProperty("secretImport"); // check for default environments expect(payload.secretImport).toEqual( @@ -102,7 +136,7 @@ describe("Secret Folder Router", async () => { position: 2, importEnv: expect.objectContaining({ name: expect.any(String), - slug: expect.any(String), + slug: expect.stringMatching(devImportDetails.envSlug), id: expect.any(String) }) }) @@ -124,28 +158,19 @@ describe("Secret Folder Router", async () => { expect(secretImportsListRes.statusCode).toBe(200); const secretImportList = JSON.parse(secretImportsListRes.payload); expect(secretImportList).toHaveProperty("secretImports"); - expect(secretImportList.secretImports[1].id).toEqual(testSecretImportId); + expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id); + expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id); + + await deleteSecretImport(createdImport1.id); + await deleteSecretImport(createdImport2.id); }); test("Delete secret import position", async () => { - const res = await testServer.inject({ - method: "DELETE", - url: `/api/v1/secret-imports/${testSecretImportId}`, - 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"); + const createdImport1 = await createSecretImport("/", "dev"); + const createdImport2 = await createSecretImport("/", "staging"); + const deletedImport = await deleteSecretImport(createdImport1.id); // check for default environments - expect(payload.secretImport).toEqual( + expect(deletedImport).toEqual( expect.objectContaining({ id: expect.any(String), importPath: expect.any(String), @@ -175,5 +200,7 @@ describe("Secret Folder Router", async () => { expect(secretImportList).toHaveProperty("secretImports"); expect(secretImportList.secretImports.length).toEqual(1); expect(secretImportList.secretImports[0].position).toEqual(1); + + await deleteSecretImport(createdImport2.id); }); }); diff --git a/backend/e2e-test/routes/v2/service-token.spec.ts b/backend/e2e-test/routes/v2/service-token.spec.ts new file mode 100644 index 000000000..a07eda4b9 --- /dev/null +++ b/backend/e2e-test/routes/v2/service-token.spec.ts @@ -0,0 +1,579 @@ +import crypto from "node:crypto"; + +import { SecretType, TSecrets } from "@app/db/schemas"; +import { decryptSecret, encryptSecret, getUserPrivateKey, seedData1 } from "@app/db/seed-data"; +import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; + +const createServiceToken = async ( + scopes: { environment: string; secretPath: string }[], + permissions: ("read" | "write")[] +) => { + const projectKeyRes = await testServer.inject({ + method: "GET", + url: `/api/v2/workspace/${seedData1.project.id}/encrypted-key`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + const projectKeyEnc = JSON.parse(projectKeyRes.payload); + + const userInfoRes = await testServer.inject({ + method: "GET", + url: "/api/v2/users/me", + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + const { user: userInfo } = JSON.parse(userInfoRes.payload); + const privateKey = await getUserPrivateKey(seedData1.password, userInfo); + const projectKey = decryptAsymmetric({ + ciphertext: projectKeyEnc.encryptedKey, + nonce: projectKeyEnc.nonce, + publicKey: projectKeyEnc.sender.publicKey, + privateKey + }); + + const randomBytes = crypto.randomBytes(16).toString("hex"); + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(projectKey, randomBytes); + const serviceTokenRes = await testServer.inject({ + method: "POST", + url: "/api/v2/service-token", + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + name: "test-token", + workspaceId: seedData1.project.id, + scopes, + encryptedKey: ciphertext, + iv, + tag, + permissions, + expiresIn: null + } + }); + expect(serviceTokenRes.statusCode).toBe(200); + const serviceTokenInfo = serviceTokenRes.json(); + expect(serviceTokenInfo).toHaveProperty("serviceToken"); + expect(serviceTokenInfo).toHaveProperty("serviceTokenData"); + return `${serviceTokenInfo.serviceToken}.${randomBytes}`; +}; + +const deleteServiceToken = async () => { + const serviceTokenListRes = await testServer.inject({ + method: "GET", + url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(serviceTokenListRes.statusCode).toBe(200); + const serviceTokens = JSON.parse(serviceTokenListRes.payload).serviceTokenData as { name: string; id: string }[]; + expect(serviceTokens.length).toBeGreaterThan(0); + const serviceTokenInfo = serviceTokens.find(({ name }) => name === "test-token"); + expect(serviceTokenInfo).toBeDefined(); + + const deleteTokenRes = await testServer.inject({ + method: "DELETE", + url: `/api/v2/service-token/${serviceTokenInfo?.id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(deleteTokenRes.statusCode).toBe(200); +}; + +const createSecret = async (dto: { + projectKey: string; + path: string; + key: string; + value: string; + comment: string; + type?: SecretType; + token: string; +}) => { + const createSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: dto.type || SecretType.Shared, + secretPath: dto.path, + ...encryptSecret(dto.projectKey, dto.key, dto.value, dto.comment) + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/${dto.key}`, + headers: { + authorization: `Bearer ${dto.token}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secret"); + return createdSecretPayload.secret; +}; + +const deleteSecret = async (dto: { path: string; key: string; token: string }) => { + const deleteSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/${dto.key}`, + headers: { + authorization: `Bearer ${dto.token}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: dto.path + } + }); + expect(deleteSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(deleteSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + return updatedSecretPayload.secret; +}; + +describe("Service token secret ops", async () => { + let serviceToken = ""; + let projectKey = ""; + let folderId = ""; + beforeAll(async () => { + serviceToken = await createServiceToken( + [{ secretPath: "/**", environment: seedData1.environment.slug }], + ["read", "write"] + ); + + // this is ensure cli service token decryptiong working fine + const serviceTokenInfoRes = await testServer.inject({ + method: "GET", + url: "/api/v2/service-token", + headers: { + authorization: `Bearer ${serviceToken}` + } + }); + expect(serviceTokenInfoRes.statusCode).toBe(200); + const serviceTokenInfo = serviceTokenInfoRes.json(); + const serviceTokenParts = serviceToken.split("."); + projectKey = decryptSymmetric128BitHexKeyUTF8({ + key: serviceTokenParts[3], + tag: serviceTokenInfo.tag, + ciphertext: serviceTokenInfo.encryptedKey, + iv: serviceTokenInfo.iv + }); + + // create a deep folder + const folderCreate = await testServer.inject({ + method: "POST", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + name: "folder", + path: "/nested1/nested2" + } + }); + expect(folderCreate.statusCode).toBe(200); + folderId = folderCreate.json().folder.id; + }); + + afterAll(async () => { + await deleteServiceToken(); + + // create a deep folder + const deleteFolder = await testServer.inject({ + method: "DELETE", + url: `/api/v1/folders/${folderId}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/nested1/nested2" + } + }); + expect(deleteFolder.statusCode).toBe(200); + }); + + const testSecrets = [ + { + path: "/", + secret: { + key: "ST-SEC", + value: "something-secret", + comment: "some comment" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "NESTED-ST-SEC", + value: "something-secret", + comment: "some comment" + } + } + ]; + + const getSecrets = async (environment: string, secretPath = "/") => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets`, + headers: { + authorization: `Bearer ${serviceToken}` + }, + query: { + secretPath, + environment, + workspaceId: seedData1.project.id + } + }); + const secrets: TSecrets[] = JSON.parse(res.payload).secrets || []; + return secrets.map((el) => ({ ...decryptSecret(projectKey, el), type: el.type })); + }; + + test.each(testSecrets)("Create secret in path $path", async ({ secret, path }) => { + const createdSecret = await createSecret({ projectKey, path, ...secret, token: serviceToken }); + const decryptedSecret = decryptSecret(projectKey, createdSecret); + expect(decryptedSecret.key).toEqual(secret.key); + expect(decryptedSecret.value).toEqual(secret.value); + expect(decryptedSecret.comment).toEqual(secret.comment); + expect(decryptedSecret.version).toEqual(1); + + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: secret.value, + type: SecretType.Shared + }) + ]) + ); + await deleteSecret({ path, key: secret.key, token: serviceToken }); + }); + + test.each(testSecrets)("Get secret by name in path $path", async ({ secret, path }) => { + await createSecret({ projectKey, path, ...secret, token: serviceToken }); + + const getSecByNameRes = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${serviceToken}` + }, + query: { + secretPath: path, + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug + } + }); + expect(getSecByNameRes.statusCode).toBe(200); + const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload); + expect(getSecretByNamePayload).toHaveProperty("secret"); + const decryptedSecret = decryptSecret(projectKey, getSecretByNamePayload.secret); + expect(decryptedSecret.key).toEqual(secret.key); + expect(decryptedSecret.value).toEqual(secret.value); + expect(decryptedSecret.comment).toEqual(secret.comment); + + await deleteSecret({ path, key: secret.key, token: serviceToken }); + }); + + test.each(testSecrets)("Update secret in path $path", async ({ path, secret }) => { + await createSecret({ projectKey, path, ...secret, token: serviceToken }); + const updateSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretPath: path, + ...encryptSecret(projectKey, secret.key, "new-value", secret.comment) + }; + const updateSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${serviceToken}` + }, + body: updateSecretReqBody + }); + expect(updateSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(updateSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + const decryptedSecret = decryptSecret(projectKey, updatedSecretPayload.secret); + expect(decryptedSecret.key).toEqual(secret.key); + expect(decryptedSecret.value).toEqual("new-value"); + expect(decryptedSecret.comment).toEqual(secret.comment); + + // list secret should have updated value + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: "new-value", + type: SecretType.Shared + }) + ]) + ); + + await deleteSecret({ path, key: secret.key, token: serviceToken }); + }); + + test.each(testSecrets)("Delete secret in path $path", async ({ secret, path }) => { + await createSecret({ projectKey, path, ...secret, token: serviceToken }); + const deletedSecret = await deleteSecret({ path, key: secret.key, token: serviceToken }); + const decryptedSecret = decryptSecret(projectKey, deletedSecret); + expect(decryptedSecret.key).toEqual(secret.key); + + // shared secret deletion should delete personal ones also + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.not.arrayContaining([ + expect.objectContaining({ + key: secret.key, + type: SecretType.Shared + }) + ]) + ); + }); + + test.each(testSecrets)("Bulk create secrets in path $path", async ({ secret, path }) => { + const createSharedSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${serviceToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}`, + ...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment) + })) + } + }); + expect(createSharedSecRes.statusCode).toBe(200); + const createSharedSecPayload = JSON.parse(createSharedSecRes.payload); + expect(createSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + key: `BULK-${secret.key}-${i + 1}`, + type: SecretType.Shared + }) + ) + ) + ); + + await Promise.all( + Array.from(Array(5)).map((_e, i) => + deleteSecret({ path, token: serviceToken, key: `BULK-${secret.key}-${i + 1}` }) + ) + ); + }); + + test.each(testSecrets)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => { + await createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-1`, path, token: serviceToken }); + + const createSharedSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${serviceToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}`, + ...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment) + })) + } + }); + expect(createSharedSecRes.statusCode).toBe(400); + + await deleteSecret({ path, key: `BULK-${secret.key}-1`, token: serviceToken }); + }); + + test.each(testSecrets)("Bulk update secrets in path $path", async ({ secret, path }) => { + await Promise.all( + Array.from(Array(5)).map((_e, i) => + createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path }) + ) + ); + + const updateSharedSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${serviceToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}`, + ...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, "update-value", secret.comment) + })) + } + }); + expect(updateSharedSecRes.statusCode).toBe(200); + const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload); + expect(updateSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + key: `BULK-${secret.key}-${i + 1}`, + value: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + await Promise.all( + Array.from(Array(5)).map((_e, i) => + deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}`, token: serviceToken }) + ) + ); + }); + + test.each(testSecrets)("Bulk delete secrets in path $path", async ({ secret, path }) => { + await Promise.all( + Array.from(Array(5)).map((_e, i) => + createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path }) + ) + ); + + const deletedSharedSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${serviceToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}` + })) + } + }); + + expect(deletedSharedSecRes.statusCode).toBe(200); + const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload); + expect(deletedSecretPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.not.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + key: `BULK-${secret.value}-${i + 1}`, + type: SecretType.Shared + }) + ) + ) + ); + }); +}); + +describe("Service token fail cases", async () => { + test("Unauthorized secret path access", async () => { + const serviceToken = await createServiceToken( + [{ secretPath: "/", environment: seedData1.environment.slug }], + ["read", "write"] + ); + const fetchSecrets = await testServer.inject({ + method: "GET", + url: "/api/v3/secrets", + query: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: "/nested/deep" + }, + headers: { + authorization: `Bearer ${serviceToken}` + } + }); + expect(fetchSecrets.statusCode).toBe(401); + expect(fetchSecrets.json().error).toBe("PermissionDenied"); + await deleteServiceToken(); + }); + + test("Unauthorized secret environment access", async () => { + const serviceToken = await createServiceToken( + [{ secretPath: "/", environment: seedData1.environment.slug }], + ["read", "write"] + ); + const fetchSecrets = await testServer.inject({ + method: "GET", + url: "/api/v3/secrets", + query: { + workspaceId: seedData1.project.id, + environment: "prod", + secretPath: "/" + }, + headers: { + authorization: `Bearer ${serviceToken}` + } + }); + expect(fetchSecrets.statusCode).toBe(401); + expect(fetchSecrets.json().error).toBe("PermissionDenied"); + await deleteServiceToken(); + }); + + test("Unauthorized write operation", async () => { + const serviceToken = await createServiceToken( + [{ secretPath: "/", environment: seedData1.environment.slug }], + ["read"] + ); + const writeSecrets = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/NEW`, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretPath: "/", + // doesn't matter project key because this will fail before that due to read only access + ...encryptSecret(crypto.randomBytes(16).toString("hex"), "NEW", "value", "") + }, + headers: { + authorization: `Bearer ${serviceToken}` + } + }); + expect(writeSecrets.statusCode).toBe(401); + expect(writeSecrets.json().error).toBe("PermissionDenied"); + + // but read access should still work fine + const fetchSecrets = await testServer.inject({ + method: "GET", + url: "/api/v3/secrets", + query: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: "/" + }, + headers: { + authorization: `Bearer ${serviceToken}` + } + }); + expect(fetchSecrets.statusCode).toBe(200); + await deleteServiceToken(); + }); +}); diff --git a/backend/e2e-test/routes/v3/secrets.spec.ts b/backend/e2e-test/routes/v3/secrets.spec.ts index e69de29bb..03e1c2f50 100644 --- a/backend/e2e-test/routes/v3/secrets.spec.ts +++ b/backend/e2e-test/routes/v3/secrets.spec.ts @@ -0,0 +1,1008 @@ +import { SecretType, TSecrets } from "@app/db/schemas"; +import { decryptSecret, encryptSecret, getUserPrivateKey, seedData1 } from "@app/db/seed-data"; +import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const createSecret = async (dto: { + projectKey: string; + path: string; + key: string; + value: string; + comment: string; + type?: SecretType; +}) => { + const createSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: dto.type || SecretType.Shared, + secretPath: dto.path, + ...encryptSecret(dto.projectKey, dto.key, dto.value, dto.comment) + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/${dto.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secret"); + return createdSecretPayload.secret; +}; + +const deleteSecret = async (dto: { path: string; key: string }) => { + const deleteSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/${dto.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: dto.path + } + }); + expect(deleteSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(deleteSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + return updatedSecretPayload.secret; +}; + +describe("Secret V3 Router", async () => { + const secretTestCases = [ + { + path: "/", + secret: { + key: "SEC1", + value: "something-secret", + comment: "some comment" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "NESTED-SEC1", + value: "something-secret", + comment: "some comment" + } + }, + { + path: "/", + secret: { + key: "secret-key-2", + value: `-----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn + hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq + fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI + ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15 + QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT + aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46 + IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie + nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi + TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw + q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj + YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP + ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7 + 6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3 + EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt + IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K + d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH + UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL + 3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2 + HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0 + PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8 + Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib + BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb + HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo + QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX + MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9 + omQDpP86RX/hIIQ+JyLSaWYa + -----END PRIVATE KEY-----`, + comment: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "secret-key-3", + value: `-----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn + hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq + fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI + ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15 + QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT + aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46 + IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie + nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi + TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw + q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj + YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP + ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7 + 6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3 + EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt + IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K + d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH + UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL + 3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2 + HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0 + PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8 + Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib + BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb + HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo + QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX + MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9 + omQDpP86RX/hIIQ+JyLSaWYa + -----END PRIVATE KEY-----`, + comment: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "secret-key-3", + value: + "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gU2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uCg==", + comment: "" + } + } + ]; + + let projectKey = ""; + let folderId = ""; + beforeAll(async () => { + const projectKeyRes = await testServer.inject({ + method: "GET", + url: `/api/v2/workspace/${seedData1.project.id}/encrypted-key`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + const projectKeyEncryptionDetails = JSON.parse(projectKeyRes.payload); + + const userInfoRes = await testServer.inject({ + method: "GET", + url: "/api/v2/users/me", + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + const { user: userInfo } = JSON.parse(userInfoRes.payload); + const privateKey = await getUserPrivateKey(seedData1.password, userInfo); + projectKey = decryptAsymmetric({ + ciphertext: projectKeyEncryptionDetails.encryptedKey, + nonce: projectKeyEncryptionDetails.nonce, + publicKey: projectKeyEncryptionDetails.sender.publicKey, + privateKey + }); + + // create a deep folder + const folderCreate = await testServer.inject({ + method: "POST", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + name: "folder", + path: "/nested1/nested2" + } + }); + expect(folderCreate.statusCode).toBe(200); + folderId = folderCreate.json().folder.id; + }); + + afterAll(async () => { + const deleteFolder = await testServer.inject({ + method: "DELETE", + url: `/api/v1/folders/${folderId}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/nested1/nested2" + } + }); + expect(deleteFolder.statusCode).toBe(200); + }); + + const getSecrets = async (environment: string, secretPath = "/") => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + query: { + secretPath, + environment, + workspaceId: seedData1.project.id + } + }); + const secrets: TSecrets[] = JSON.parse(res.payload).secrets || []; + return secrets.map((el) => ({ ...decryptSecret(projectKey, el), type: el.type })); + }; + + test.each(secretTestCases)("Create secret in path $path", async ({ secret, path }) => { + const createdSecret = await createSecret({ projectKey, path, ...secret }); + const decryptedSecret = decryptSecret(projectKey, createdSecret); + expect(decryptedSecret.key).toEqual(secret.key); + expect(decryptedSecret.value).toEqual(secret.value); + expect(decryptedSecret.comment).toEqual(secret.comment); + expect(decryptedSecret.version).toEqual(1); + + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: secret.value, + type: SecretType.Shared + }) + ]) + ); + await deleteSecret({ path, key: secret.key }); + }); + + test.each(secretTestCases)("Get secret by name in path $path", async ({ secret, path }) => { + await createSecret({ projectKey, path, ...secret }); + + const getSecByNameRes = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + query: { + secretPath: path, + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug + } + }); + expect(getSecByNameRes.statusCode).toBe(200); + const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload); + expect(getSecretByNamePayload).toHaveProperty("secret"); + const decryptedSecret = decryptSecret(projectKey, getSecretByNamePayload.secret); + expect(decryptedSecret.key).toEqual(secret.key); + expect(decryptedSecret.value).toEqual(secret.value); + expect(decryptedSecret.comment).toEqual(secret.comment); + + await deleteSecret({ path, key: secret.key }); + }); + + test.each(secretTestCases)( + "Creating personal secret without shared throw error in path $path", + async ({ secret }) => { + const createSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Personal, + ...encryptSecret(projectKey, "SEC2", secret.value, secret.comment) + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/SEC2`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createSecretReqBody + }); + const payload = JSON.parse(createSecRes.payload); + expect(createSecRes.statusCode).toBe(400); + expect(payload.error).toEqual("BadRequest"); + expect(payload.message).toEqual("Failed to create personal secret override for no corresponding shared secret"); + } + ); + + test.each(secretTestCases)("Creating personal secret in path $path", async ({ secret, path }) => { + await createSecret({ projectKey, path, ...secret }); + + const createSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Personal, + secretPath: path, + ...encryptSecret(projectKey, secret.key, "personal-value", secret.comment) + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + + // list secrets should contain personal one and shared one + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: secret.value, + type: SecretType.Shared + }), + expect.objectContaining({ + key: secret.key, + value: "personal-value", + type: SecretType.Personal + }) + ]) + ); + + await deleteSecret({ path, key: secret.key }); + }); + + test.each(secretTestCases)("Update secret in path $path", async ({ path, secret }) => { + await createSecret({ projectKey, path, ...secret }); + const updateSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretPath: path, + ...encryptSecret(projectKey, secret.key, "new-value", secret.comment) + }; + const updateSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/${secret.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: updateSecretReqBody + }); + expect(updateSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(updateSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + const decryptedSecret = decryptSecret(projectKey, updatedSecretPayload.secret); + expect(decryptedSecret.key).toEqual(secret.key); + expect(decryptedSecret.value).toEqual("new-value"); + expect(decryptedSecret.comment).toEqual(secret.comment); + + // list secret should have updated value + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: "new-value", + type: SecretType.Shared + }) + ]) + ); + + await deleteSecret({ path, key: secret.key }); + }); + + test.each(secretTestCases)("Delete secret in path $path", async ({ secret, path }) => { + await createSecret({ projectKey, path, ...secret }); + const deletedSecret = await deleteSecret({ path, key: secret.key }); + const decryptedSecret = decryptSecret(projectKey, deletedSecret); + expect(decryptedSecret.key).toEqual(secret.key); + + // shared secret deletion should delete personal ones also + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.not.arrayContaining([ + expect.objectContaining({ + key: secret.key, + type: SecretType.Shared + }), + expect.objectContaining({ + key: secret.key, + type: SecretType.Personal + }) + ]) + ); + }); + + test.each(secretTestCases)( + "Deleting personal one should not delete shared secret in path $path", + async ({ secret, path }) => { + await createSecret({ projectKey, path, ...secret }); // shared one + await createSecret({ projectKey, path, ...secret, type: SecretType.Personal }); + + // shared secret deletion should delete personal ones also + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + type: SecretType.Shared + }), + expect.not.objectContaining({ + key: secret.key, + type: SecretType.Personal + }) + ]) + ); + await deleteSecret({ path, key: secret.key }); + } + ); + + test.each(secretTestCases)("Bulk create secrets in path $path", async ({ secret, path }) => { + const createSharedSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}`, + ...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment) + })) + } + }); + expect(createSharedSecRes.statusCode).toBe(200); + const createSharedSecPayload = JSON.parse(createSharedSecRes.payload); + expect(createSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + key: `BULK-${secret.key}-${i + 1}`, + type: SecretType.Shared + }) + ) + ) + ); + + await Promise.all(Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))); + }); + + test.each(secretTestCases)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => { + await createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-1`, path }); + + const createSharedSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}`, + ...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment) + })) + } + }); + expect(createSharedSecRes.statusCode).toBe(400); + + await deleteSecret({ path, key: `BULK-${secret.key}-1` }); + }); + + test.each(secretTestCases)("Bulk update secrets in path $path", async ({ secret, path }) => { + await Promise.all( + Array.from(Array(5)).map((_e, i) => + createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-${i + 1}`, path }) + ) + ); + + const updateSharedSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}`, + ...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, "update-value", secret.comment) + })) + } + }); + expect(updateSharedSecRes.statusCode).toBe(200); + const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload); + expect(updateSharedSecPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + key: `BULK-${secret.key}-${i + 1}`, + value: "update-value", + type: SecretType.Shared + }) + ) + ) + ); + await Promise.all(Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))); + }); + + test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => { + await Promise.all( + Array.from(Array(5)).map((_e, i) => + createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-${i + 1}`, path }) + ) + ); + + const deletedSharedSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/batch`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path, + secrets: Array.from(Array(5)).map((_e, i) => ({ + secretName: `BULK-${secret.key}-${i + 1}` + })) + } + }); + + expect(deletedSharedSecRes.statusCode).toBe(200); + const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload); + expect(deletedSecretPayload).toHaveProperty("secrets"); + + // bulk ones should exist + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.not.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ + key: `BULK-${secret.value}-${i + 1}`, + type: SecretType.Shared + }) + ) + ) + ); + }); +}); + +const createRawSecret = async (dto: { + path: string; + key: string; + value: string; + comment: string; + type?: SecretType; +}) => { + const createSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: dto.type || SecretType.Shared, + secretValue: dto.value, + secretComment: dto.comment, + secretPath: dto.path + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secret"); + return createdSecretPayload.secret; +}; + +const deleteRawSecret = async (dto: { path: string; key: string }) => { + const deleteSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: dto.path + } + }); + expect(deleteSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(deleteSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + return updatedSecretPayload.secret; +}; + +// raw secret endpoints +describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }])( + "Secret V3 Raw Router - $auth mode", + async ({ auth }) => { + let folderId = ""; + let authToken = ""; + const testRawSecrets = [ + { + path: "/", + secret: { + key: "RAW-SEC1", + value: "something-secret", + comment: "some comment" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: "NESTED-RAW-SEC1", + value: "something-secret", + comment: "some comment" + } + } + ]; + + beforeAll(async () => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v2/workspace/${seedData1.project.id}/encrypted-key`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(res.statusCode).toEqual(200); + const projectKeyEnc = JSON.parse(res.payload); + + const userInfoRes = await testServer.inject({ + method: "GET", + url: "/api/v2/users/me", + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(userInfoRes.statusCode).toEqual(200); + const { user: userInfo } = JSON.parse(userInfoRes.payload); + + const privateKey = await getUserPrivateKey(seedData1.password, userInfo); + const projectKey = decryptAsymmetric({ + ciphertext: projectKeyEnc.encryptedKey, + nonce: projectKeyEnc.nonce, + publicKey: projectKeyEnc.sender.publicKey, + privateKey + }); + + const projectBotRes = await testServer.inject({ + method: "GET", + url: `/api/v1/bot/${seedData1.project.id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(projectBotRes.statusCode).toEqual(200); + const projectBot = JSON.parse(projectBotRes.payload).bot; + const botKey = encryptAsymmetric(projectKey, projectBot.publicKey, privateKey); + + // set bot as active + const setBotActive = await testServer.inject({ + method: "PATCH", + url: `/api/v1/bot/${projectBot.id}/active`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + isActive: true, + workspaceId: seedData1.project.id, + botKey: { + encryptedKey: botKey.ciphertext, + nonce: botKey.nonce + } + } + }); + expect(setBotActive.statusCode).toEqual(200); + + // create a deep folder + const folderCreate = await testServer.inject({ + method: "POST", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + name: "folder", + path: "/nested1/nested2" + } + }); + expect(folderCreate.statusCode).toBe(200); + folderId = folderCreate.json().folder.id; + + if (auth === AuthMode.JWT) { + authToken = jwtAuthToken; + } else if (auth === AuthMode.IDENTITY_ACCESS_TOKEN) { + const identityLogin = await testServer.inject({ + method: "POST", + url: "/api/v1/auth/universal-auth/login", + body: { + clientSecret: seedData1.machineIdentity.clientCredentials.secret, + clientId: seedData1.machineIdentity.clientCredentials.id + } + }); + expect(identityLogin.statusCode).toBe(200); + authToken = identityLogin.json().accessToken; + } + }); + + afterAll(async () => { + const projectBotRes = await testServer.inject({ + method: "GET", + url: `/api/v1/bot/${seedData1.project.id}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + } + }); + expect(projectBotRes.statusCode).toEqual(200); + const projectBot = JSON.parse(projectBotRes.payload).bot; + + // set bot as inactive + const setBotInActive = await testServer.inject({ + method: "PATCH", + url: `/api/v1/bot/${projectBot.id}/active`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + isActive: false, + workspaceId: seedData1.project.id + } + }); + expect(setBotInActive.statusCode).toEqual(200); + const deleteFolder = await testServer.inject({ + method: "DELETE", + url: `/api/v1/folders/${folderId}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + path: "/nested1/nested2" + } + }); + expect(deleteFolder.statusCode).toBe(200); + }); + + const getSecrets = async (environment: string, secretPath = "/") => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/raw`, + headers: { + authorization: `Bearer ${authToken}` + }, + query: { + secretPath, + environment, + workspaceId: seedData1.project.id + } + }); + const secrets: { secretKey: string; secretValue: string; type: SecretType; version: number }[] = + JSON.parse(res.payload).secrets || []; + return secrets.map((el) => ({ key: el.secretKey, value: el.secretValue, type: el.type, version: el.version })); + }; + + test.each(testRawSecrets)("Create secret raw in path $path", async ({ secret, path }) => { + const createSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretValue: secret.value, + secretComment: secret.comment, + secretPath: path + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/raw/${secret.key}`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secret"); + + // fetch secrets + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: secret.value, + type: SecretType.Shared + }) + ]) + ); + + await deleteRawSecret({ path, key: secret.key }); + }); + + test.each(testRawSecrets)("Get secret by name raw in path $path", async ({ secret, path }) => { + await createRawSecret({ path, ...secret }); + + const getSecByNameRes = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/raw/${secret.key}`, + headers: { + authorization: `Bearer ${authToken}` + }, + query: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + secretPath: path + } + }); + expect(getSecByNameRes.statusCode).toBe(200); + const secretPayload = JSON.parse(getSecByNameRes.payload); + expect(secretPayload).toHaveProperty("secret"); + expect(secretPayload.secret).toEqual( + expect.objectContaining({ + secretKey: secret.key, + secretValue: secret.value + }) + ); + + await deleteRawSecret({ path, key: secret.key }); + }); + + test.each(testRawSecrets)("List secret raw in path $path", async ({ secret, path }) => { + await Promise.all( + Array.from(Array(5)).map((_e, i) => createRawSecret({ path, ...secret, key: `BULK-${secret.key}-${i + 1}` })) + ); + + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets.length).toEqual(5); + expect(secrets).toEqual( + expect.arrayContaining( + Array.from(Array(5)).map((_e, i) => + expect.objectContaining({ value: expect.any(String), key: `BULK-${secret.key}-${i + 1}` }) + ) + ) + ); + + await Promise.all( + Array.from(Array(5)).map((_e, i) => deleteRawSecret({ path, key: `BULK-${secret.key}-${i + 1}` })) + ); + }); + + test.each(testRawSecrets)("Update secret raw in path $path", async ({ secret, path }) => { + await createRawSecret({ path, ...secret }); + + const updateSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretValue: "new-value", + secretPath: path + }; + const updateSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/raw/${secret.key}`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: updateSecretReqBody + }); + expect(updateSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(updateSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + + // fetch secrets + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: secret.key, + value: "new-value", + version: 2, + type: SecretType.Shared + }) + ]) + ); + + await deleteRawSecret({ path, key: secret.key }); + }); + + test.each(testRawSecrets)("Delete secret raw in path $path", async ({ path, secret }) => { + await createRawSecret({ path, ...secret }); + + const deletedSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretPath: path + }; + const deletedSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/raw/${secret.key}`, + headers: { + authorization: `Bearer ${authToken}` + }, + body: deletedSecretReqBody + }); + expect(deletedSecRes.statusCode).toBe(200); + const deletedSecretPayload = JSON.parse(deletedSecRes.payload); + expect(deletedSecretPayload).toHaveProperty("secret"); + + // fetch secrets + const secrets = await getSecrets(seedData1.environment.slug, path); + expect(secrets).toEqual([]); + }); + } +); + +describe("Secret V3 Raw Router Without E2EE enabled", async () => { + const secret = { + key: "RAW-SEC-1", + value: "something-secret", + comment: "some comment" + }; + + test("Create secret raw", async () => { + const createSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretValue: secret.value, + secretComment: secret.comment + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/raw/${secret.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(400); + }); + + test("Update secret raw", async () => { + const updateSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared, + secretValue: "new-value" + }; + const updateSecRes = await testServer.inject({ + method: "PATCH", + url: `/api/v3/secrets/raw/${secret.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: updateSecretReqBody + }); + expect(updateSecRes.statusCode).toBe(400); + }); + + test("Delete secret raw", async () => { + const deletedSecretReqBody = { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + type: SecretType.Shared + }; + const deletedSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/raw/${secret.key}`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: deletedSecretReqBody + }); + expect(deletedSecRes.statusCode).toBe(400); + }); +}); diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 1e424401e..afb1bbabe 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -1,26 +1,30 @@ -// import { main } from "@app/server/app"; -import { initEnvConfig } from "@app/lib/config/env"; +// eslint-disable-next-line +import "ts-node/register"; + import dotenv from "dotenv"; +import jwt from "jsonwebtoken"; import knex from "knex"; import path from "path"; -import { mockSmtpServer } from "./mocks/smtp"; -import { initLogger } from "@app/lib/logger"; -import jwt from "jsonwebtoken"; -import "ts-node/register"; -import { main } from "@app/server/app"; -import { mockQueue } from "./mocks/queue"; -import { AuthTokenType } from "@app/services/auth/auth-type"; import { seedData1 } from "@app/db/seed-data"; +import { initEnvConfig } from "@app/lib/config/env"; +import { initLogger } from "@app/lib/logger"; +import { main } from "@app/server/app"; +import { AuthTokenType } from "@app/services/auth/auth-type"; -dotenv.config({ path: path.join(__dirname, "../.env.test") }); +import { mockQueue } from "./mocks/queue"; +import { mockSmtpServer } from "./mocks/smtp"; + +dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { name: "knex-env", transformMode: "ssr", async setup() { + const logger = await initLogger(); + const cfg = initEnvConfig(logger); const db = knex({ client: "pg", - connection: process.env.DB_CONNECTION_URI, + connection: cfg.DB_CONNECTION_URI, migrations: { directory: path.join(__dirname, "../src/db/migrations"), extension: "ts", @@ -37,8 +41,6 @@ export default { await db.seed.run(); const smtp = mockSmtpServer(); const queue = mockQueue(); - const logger = await initLogger(); - const cfg = initEnvConfig(logger); const server = await main({ db, smtp, logger, queue }); // @ts-expect-error type globalThis.testServer = server; @@ -54,6 +56,7 @@ export default { { expiresIn: cfg.JWT_AUTH_LIFETIME } ); } catch (error) { + console.log("[TEST] Error setting up environment", error); await db.destroy(); throw error; } diff --git a/backend/package-lock.json b/backend/package-lock.json index 1bce540c6..fc535212a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.485.0", + "@aws-sdk/client-secrets-manager": "^3.502.0", "@casl/ability": "^6.5.0", "@fastify/cookie": "^9.2.0", "@fastify/cors": "^8.4.1", @@ -19,8 +19,8 @@ "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", "@fastify/session": "^10.7.0", - "@fastify/swagger": "^8.12.0", - "@fastify/swagger-ui": "^1.10.1", + "@fastify/swagger": "^8.14.0", + "@fastify/swagger-ui": "^2.1.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", @@ -29,13 +29,13 @@ "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", - "aws-sdk": "^2.1532.0", - "axios": "^1.6.2", + "aws-sdk": "^2.1545.0", + "axios": "^1.6.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", - "bullmq": "^5.1.1", + "bullmq": "^5.1.6", "dotenv": "^16.3.1", - "fastify": "^4.24.3", + "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", "handlebars": "^4.7.8", "ioredis": "^5.3.2", @@ -45,10 +45,11 @@ "knex": "^3.0.1", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", - "mysql2": "^3.6.5", + "mysql2": "^3.9.1", "nanoid": "^5.0.4", "node-cache": "^5.1.2", - "nodemailer": "^6.9.7", + "nodemailer": "^6.9.9", + "ora": "^7.0.1", "passport-github": "^1.1.0", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", @@ -56,13 +57,13 @@ "picomatch": "^3.0.1", "pino": "^8.16.2", "posthog-node": "^3.6.0", - "probot": "^12.3.3", + "probot": "^13.0.0", "smee-client": "^2.0.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "uuid": "^9.0.1", "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.0" + "zod-to-json-schema": "^3.22.4" }, "devDependencies": { "@types/bcrypt": "^5.0.2", @@ -101,7 +102,7 @@ "tsx": "^4.4.0", "typescript": "^5.3.2", "vite-tsconfig-paths": "^4.2.2", - "vitest": "^1.0.4" + "vitest": "^1.2.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -355,22 +356,6 @@ "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/core": { - "version": "3.496.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.496.0.tgz", - "integrity": "sha512-yT+ug7Cw/3eJi7x2es0+46x12+cIJm5Xv+GPWsrTFD1TKgqO/VPEgfDtHFagDNbFmjNQA65Ygc/kEdIX9ICX/A==", - "dependencies": { - "@smithy/core": "^1.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/signature-v4": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-env": { "version": "3.496.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.496.0.tgz", @@ -676,49 +661,49 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.485.0.tgz", - "integrity": "sha512-TruRGEdTy1y/5ln1NcU5LvIZyK38O89zU9vCfNQIKwTSrpS0sDJQukjg8VfMC8gbqUUvXdiPcS61Fxr1WfWn7g==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.502.0.tgz", + "integrity": "sha512-ICU084A/EbYMqca6NVFqeMtHh+KCdn0H7UjARUy5ur1yOlXXvxqAJGtKZDYFjuEO08F30zbv7+4HCOy6yjOJ0Q==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.485.0", - "@aws-sdk/core": "3.485.0", - "@aws-sdk/credential-provider-node": "3.485.0", - "@aws-sdk/middleware-host-header": "3.485.0", - "@aws-sdk/middleware-logger": "3.485.0", - "@aws-sdk/middleware-recursion-detection": "3.485.0", - "@aws-sdk/middleware-signing": "3.485.0", - "@aws-sdk/middleware-user-agent": "3.485.0", - "@aws-sdk/region-config-resolver": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@aws-sdk/util-user-agent-browser": "3.485.0", - "@aws-sdk/util-user-agent-node": "3.485.0", - "@smithy/config-resolver": "^2.0.23", - "@smithy/core": "^1.2.2", - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/hash-node": "^2.0.18", - "@smithy/invalid-dependency": "^2.0.16", - "@smithy/middleware-content-length": "^2.0.18", - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/protocol-http": "^3.0.12", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-body-length-browser": "^2.0.1", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.24", - "@smithy/util-defaults-mode-node": "^2.0.32", - "@smithy/util-endpoints": "^1.0.8", - "@smithy/util-retry": "^2.0.9", - "@smithy/util-utf8": "^2.0.2", + "@aws-sdk/client-sts": "3.502.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/credential-provider-node": "3.502.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-signing": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0", "uuid": "^8.3.2" }, @@ -735,112 +720,166 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.485.0.tgz", - "integrity": "sha512-apN2bEn0PZs0jD4jAfvwO3dlWqw9YIQJ6TAudM1bd3S5vzWqlBBcLfQpK6taHoQaI+WqgUWXLuOf7gRFbGXKPg==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.502.0.tgz", + "integrity": "sha512-OZAYal1+PQgUUtWiHhRayDtX0OD+XpXHKAhjYgEIPbyhQaCMp3/Bq1xDX151piWXvXqXLJHFKb8DUEqzwGO9QA==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.485.0", - "@aws-sdk/middleware-host-header": "3.485.0", - "@aws-sdk/middleware-logger": "3.485.0", - "@aws-sdk/middleware-recursion-detection": "3.485.0", - "@aws-sdk/middleware-user-agent": "3.485.0", - "@aws-sdk/region-config-resolver": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@aws-sdk/util-user-agent-browser": "3.485.0", - "@aws-sdk/util-user-agent-node": "3.485.0", - "@smithy/config-resolver": "^2.0.23", - "@smithy/core": "^1.2.2", - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/hash-node": "^2.0.18", - "@smithy/invalid-dependency": "^2.0.16", - "@smithy/middleware-content-length": "^2.0.18", - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/protocol-http": "^3.0.12", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-body-length-browser": "^2.0.1", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.24", - "@smithy/util-defaults-mode-node": "^2.0.32", - "@smithy/util-endpoints": "^1.0.8", - "@smithy/util-retry": "^2.0.9", - "@smithy/util-utf8": "^2.0.2", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.485.0.tgz", - "integrity": "sha512-PI4q36kVF0fpIPZyeQhrwwJZ6SRkOGvU3rX5Qn4b5UY5X+Ct1aLhqSX8/OB372UZIcnh6eSvERu8POHleDO7Jw==", + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.502.0.tgz", + "integrity": "sha512-Yc9tZqTOMWtdgpkrdjKShgWb9oKNsFQrItfoiN1xWDllaFFRPi2KTiZiR0AbSTrNasJy13d210DOxrIdte+kWQ==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.485.0", - "@aws-sdk/credential-provider-node": "3.485.0", - "@aws-sdk/middleware-host-header": "3.485.0", - "@aws-sdk/middleware-logger": "3.485.0", - "@aws-sdk/middleware-recursion-detection": "3.485.0", - "@aws-sdk/middleware-user-agent": "3.485.0", - "@aws-sdk/region-config-resolver": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@aws-sdk/util-user-agent-browser": "3.485.0", - "@aws-sdk/util-user-agent-node": "3.485.0", - "@smithy/config-resolver": "^2.0.23", - "@smithy/core": "^1.2.2", - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/hash-node": "^2.0.18", - "@smithy/invalid-dependency": "^2.0.16", - "@smithy/middleware-content-length": "^2.0.18", - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/protocol-http": "^3.0.12", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-body-length-browser": "^2.0.1", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.24", - "@smithy/util-defaults-mode-node": "^2.0.32", - "@smithy/util-endpoints": "^1.0.8", - "@smithy/util-middleware": "^2.0.9", - "@smithy/util-retry": "^2.0.9", - "@smithy/util-utf8": "^2.0.2", + "@aws-sdk/client-sts": "3.502.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-signing": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "*" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.502.0.tgz", + "integrity": "sha512-0q08gsvn6nuRqjK+i/e30PT/t7vvYwmGJS0PhJikZWv5yRDNSUxSYG0uDwKSbLDzmc2UX5+mLeyjPHlL4hbGlA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", "fast-xml-parser": "4.2.5", "tslib": "^2.5.0" }, "engines": { "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "*" } }, "node_modules/@aws-sdk/core": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.485.0.tgz", - "integrity": "sha512-Yvi80DQcbjkYCft471ClE3HuetuNVqntCs6eFOomDcrJaqdOFrXv2kJAxky84MRA/xb7bGlDGAPbTuj1ICputg==", + "version": "3.496.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.496.0.tgz", + "integrity": "sha512-yT+ug7Cw/3eJi7x2es0+46x12+cIJm5Xv+GPWsrTFD1TKgqO/VPEgfDtHFagDNbFmjNQA65Ygc/kEdIX9ICX/A==", "dependencies": { - "@smithy/core": "^1.2.2", - "@smithy/protocol-http": "^3.0.12", - "@smithy/signature-v4": "^2.0.0", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", + "@smithy/core": "^1.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/signature-v4": "^2.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -848,13 +887,13 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.485.0.tgz", - "integrity": "sha512-3XkFgwVU1XOB33dV7t9BKJ/ptdl2iS+0dxE7ecq8aqT2/gsfKmLCae1G17P8WmdD3z0kMDTvnqM2aWgUnSOkmg==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.502.0.tgz", + "integrity": "sha512-KIB8Ae1Z7domMU/jU4KiIgK4tmYgvuXlhR54ehwlVHxnEoFPoPuGHFZU7oFn79jhhSLUFQ1lRYMxP0cEwb7XeQ==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -862,19 +901,20 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.485.0.tgz", - "integrity": "sha512-cFYF/Bdw7EnT4viSxYpNIv3IBkri/Yb+JpQXl8uDq7bfVJfAN5qZmK07vRkg08xL6TC4F41wshhMSAucGdTwIw==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.502.0.tgz", + "integrity": "sha512-1wB/escbspUY6uRDEMp9AMMyypUSyuQ0AMO1yQNtXviV8cPf+CuRbqP/UVnimHO1RuX0n5BmjDVVjUIEU6kuGA==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.485.0", - "@aws-sdk/credential-provider-process": "3.485.0", - "@aws-sdk/credential-provider-sso": "3.485.0", - "@aws-sdk/credential-provider-web-identity": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@smithy/credential-provider-imds": "^2.0.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", + "@aws-sdk/client-sts": "3.502.0", + "@aws-sdk/credential-provider-env": "3.502.0", + "@aws-sdk/credential-provider-process": "3.502.0", + "@aws-sdk/credential-provider-sso": "3.502.0", + "@aws-sdk/credential-provider-web-identity": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -882,20 +922,20 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.485.0.tgz", - "integrity": "sha512-2DwzO2azkSzngifKDT61W/DL0tSzewuaFHiLJWdfc8Et3mdAQJ9x3KAj8u7XFpjIcGNqk7FiKjN+zeGUuNiEhA==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.502.0.tgz", + "integrity": "sha512-qg71UpYeFrjhu5hD+vdRqZ+EYFB11BeszsbfEJGaHhOMHmmTHNBaDAexW+bUnJSXcJL0a8vniCvca+rElbcAHQ==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.485.0", - "@aws-sdk/credential-provider-ini": "3.485.0", - "@aws-sdk/credential-provider-process": "3.485.0", - "@aws-sdk/credential-provider-sso": "3.485.0", - "@aws-sdk/credential-provider-web-identity": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@smithy/credential-provider-imds": "^2.0.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", + "@aws-sdk/credential-provider-env": "3.502.0", + "@aws-sdk/credential-provider-ini": "3.502.0", + "@aws-sdk/credential-provider-process": "3.502.0", + "@aws-sdk/credential-provider-sso": "3.502.0", + "@aws-sdk/credential-provider-web-identity": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@smithy/credential-provider-imds": "^2.2.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -903,14 +943,14 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.485.0.tgz", - "integrity": "sha512-X9qS6ZO/rDKYDgWqD1YmSX7sAUUHax9HbXlgGiTTdtfhZvQh1ZmnH6wiPu5WNliafHZFtZT2W07kgrDLPld/Ug==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.502.0.tgz", + "integrity": "sha512-fJJowOjQ4infYQX0E1J3xFVlmuwEYJAFk0Mo1qwafWmEthsBJs+6BR2RiWDELHKrSK35u4Pf3fu3RkYuCtmQFw==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -918,16 +958,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.485.0.tgz", - "integrity": "sha512-l0oC8GTrWh+LFQQfSmG1Jai1PX7Mhj9arb/CaS1/tmeZE0hgIXW++tvljYs/Dds4LGXUlaWG+P7BrObf6OyIXA==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.502.0.tgz", + "integrity": "sha512-/2Nyvo+cWQpH283lmZBimTJ9JDhES9FzQUkhUXZgxQo3Ez4sguLVi2V9xoFFyG0cMff5fuNivdKHfj4FeMGjZw==", "dependencies": { - "@aws-sdk/client-sso": "3.485.0", - "@aws-sdk/token-providers": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", + "@aws-sdk/client-sso": "3.502.0", + "@aws-sdk/token-providers": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -935,13 +975,14 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.485.0.tgz", - "integrity": "sha512-WpBFZFE0iXtnibH5POMEKITj/hR0YV5l2n9p8BEvKjdJ63s3Xke1RN20ZdIyKDaRDwj8adnKDgNPEnAKdS4kLw==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.502.0.tgz", + "integrity": "sha512-veBAjDqjMMgA2Qxxf9ywDfHYLeJpaeHWLWCQ9XCHwJJ6ZIGWmAZPTq3he/UMr5JIQXooIccqqyqXMDIXPenXpA==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/types": "^2.8.0", + "@aws-sdk/client-sts": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -949,13 +990,13 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.485.0.tgz", - "integrity": "sha512-1mAUX9dQNGo2RIKseVj7SI/D5abQJQ/Os8hQ0NyVAyyVYF+Yjx5PphKgfhM5yoBwuwZUl6q71XPYEGNx7be6SA==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.502.0.tgz", + "integrity": "sha512-EjnG0GTYXT/wJBmm5/mTjDcAkzU8L7wQjOzd3FTXuTCNNyvAvwrszbOj5FlarEw5XJBbQiZtBs+I5u9+zy560w==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -963,12 +1004,12 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.485.0.tgz", - "integrity": "sha512-O8IgJ0LHi5wTs5GlpI7nqmmSSagkVdd1shpGgQWY2h0kMSCII8CJZHBG97dlFFpGTvx5EDlhPNek7rl/6F4dRw==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.502.0.tgz", + "integrity": "sha512-FDyv6K4nCoHxbjLGS2H8ex8I0KDIiu4FJgVRPs140ZJy6gE5Pwxzv6YTzZGLMrnqcIs9gh065Lf6DjwMelZqaw==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -976,13 +1017,13 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.485.0.tgz", - "integrity": "sha512-ZeVNATGNFcqkWDut3luVszROTUzkU5u+rJpB/xmeMoenlDAjPRiHt/ca3WkI5wAnIJ1VSNGpD2sOFLMCH+EWag==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.502.0.tgz", + "integrity": "sha512-hvbyGJbxeuezxOu8VfFmcV4ql1hKXLxHTe5FNYfEBat2KaZXVhc1Hg+4TvB06/53p+E8J99Afmumkqbxs2esUA==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -990,16 +1031,16 @@ } }, "node_modules/@aws-sdk/middleware-signing": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.485.0.tgz", - "integrity": "sha512-41xzT2p1sOibhsLkdE5rwPJkNbBtKD8Gp36/ySfu0KE415wfXKacElSVxAaBw39/j7iSWDYqqybeEYbAzk+3GQ==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.502.0.tgz", + "integrity": "sha512-4hF08vSzJ7L6sB+393gOFj3s2N6nLusYS0XrMW6wYNFU10IDdbf8Z3TZ7gysDJJHEGQPmTAesPEDBsasGWcMxg==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/signature-v4": "^2.0.0", - "@smithy/types": "^2.8.0", - "@smithy/util-middleware": "^2.0.9", + "@aws-sdk/types": "3.502.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/signature-v4": "^2.1.1", + "@smithy/types": "^2.9.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -1007,14 +1048,14 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.485.0.tgz", - "integrity": "sha512-CddCVOn+OPQ0CcchketIg+WF6v+MDLAf3GOYTR2htUxxIm7HABuRd6R3kvQ5Jny9CV8gMt22G1UZITsFexSJlQ==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.502.0.tgz", + "integrity": "sha512-TxbBZbRiXPH0AUxegqiNd9aM9zNSbfjtBs5MEfcBsweeT/B2O7K1EjP9+CkB8Xmk/5FLKhAKLr19b1TNoE27rw==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@smithy/protocol-http": "^3.1.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -1022,14 +1063,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.485.0.tgz", - "integrity": "sha512-2FB2EQ0sIE+YgFqGtkE1lDIMIL6nYe6MkOHBwBM7bommadKIrbbr2L22bPZGs3ReTsxiJabjzxbuCAVhrpHmhg==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.502.0.tgz", + "integrity": "sha512-mxmsX2AGgnSM+Sah7mcQCIneOsJQNiLX0COwEttuf8eO+6cLMAZvVudH3BnWTfea4/A9nuri9DLCqBvEmPrilg==", "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", - "@smithy/util-config-provider": "^2.1.0", - "@smithy/util-middleware": "^2.0.9", + "@aws-sdk/types": "3.502.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -1037,46 +1079,15 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.485.0.tgz", - "integrity": "sha512-kOXA1WKIVIFNRqHL8ynVZ3hCKLsgnEmGr2iDR6agDNw5fYIlCO/6N2xR6QdGcLTvUUbwOlz4OvKLUQnWMKAnnA==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.502.0.tgz", + "integrity": "sha512-RQgMgIXYlSf0xGl6EUeD+pqIPBlb7e29dbqHOBFc66hJVYUC2ULZX7Y+jLvcGIEaMiIaTPyvntZRFip+U+9hag==", "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/middleware-host-header": "3.485.0", - "@aws-sdk/middleware-logger": "3.485.0", - "@aws-sdk/middleware-recursion-detection": "3.485.0", - "@aws-sdk/middleware-user-agent": "3.485.0", - "@aws-sdk/region-config-resolver": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@aws-sdk/util-user-agent-browser": "3.485.0", - "@aws-sdk/util-user-agent-node": "3.485.0", - "@smithy/config-resolver": "^2.0.23", - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/hash-node": "^2.0.18", - "@smithy/invalid-dependency": "^2.0.16", - "@smithy/middleware-content-length": "^2.0.18", - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/property-provider": "^2.0.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-body-length-browser": "^2.0.1", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.24", - "@smithy/util-defaults-mode-node": "^2.0.32", - "@smithy/util-endpoints": "^1.0.8", - "@smithy/util-retry": "^2.0.9", - "@smithy/util-utf8": "^2.0.2", + "@aws-sdk/client-sso-oidc": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@smithy/property-provider": "^2.1.1", + "@smithy/shared-ini-file-loader": "^2.3.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -1084,11 +1095,11 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.485.0.tgz", - "integrity": "sha512-+QW32YQdvZRDOwrAQPo/qCyXoSjgXB6RwJwCwkd8ebJXRXw6tmGKIHaZqYHt/LtBymvnaBgBBADNa4+qFvlOFw==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.502.0.tgz", + "integrity": "sha512-M0DSPYe/gXhwD2QHgoukaZv5oDxhW3FfvYIrJptyqUq3OnPJBcDbihHjrE0PBtfh/9kgMZT60/fQ2NVFANfa2g==", "dependencies": { - "@smithy/types": "^2.8.0", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -1096,12 +1107,13 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.485.0.tgz", - "integrity": "sha512-dTd642F7nJisApF8YjniqQ6U59CP/DCtar11fXf1nG9YNBCBsNNVw5ZfZb5nSNzaIdy27mQioWTCV18JEj1mxg==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.502.0.tgz", + "integrity": "sha512-6LKFlJPp2J24r1Kpfoz5ESQn+1v5fEjDB3mtUKRdpwarhm3syu7HbKlHCF3KbcCOyahobvLvhoedT78rJFEeeg==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/util-endpoints": "^1.0.8", + "@aws-sdk/types": "3.502.0", + "@smithy/types": "^2.9.1", + "@smithy/util-endpoints": "^1.1.1", "tslib": "^2.5.0" }, "engines": { @@ -1120,24 +1132,24 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.485.0.tgz", - "integrity": "sha512-QliWbjg0uOhGTcWgWTKPMY0SBi07g253DjwrCINT1auqDrdQPxa10xozpZExBYjAK2KuhYDNUzni127ae6MHOw==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.502.0.tgz", + "integrity": "sha512-v8gKyCs2obXoIkLETAeEQ3AM+QmhHhst9xbM1cJtKUGsRlVIak/XyyD+kVE6kmMm1cjfudHpHKABWk9apQcIZQ==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@smithy/types": "^2.9.1", "bowser": "^2.11.0", "tslib": "^2.5.0" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.485.0.tgz", - "integrity": "sha512-QF+aQ9jnDlPUlFBxBRqOylPf86xQuD3aEPpOErR+50qJawVvKa94uiAFdvtI9jv6hnRZmuFsTj2rsyytnbAYBA==", + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.502.0.tgz", + "integrity": "sha512-9RjxpkGZKbTdl96tIJvAo+vZoz4P/cQh36SBUt9xfRfW0BtsaLyvSrvlR5wyUYhvRcC12Axqh/8JtnAPq//+Vw==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", + "@aws-sdk/types": "3.502.0", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/types": "^2.9.1", "tslib": "^2.5.0" }, "engines": { @@ -1778,9 +1790,9 @@ } }, "node_modules/@fastify/swagger": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-8.12.0.tgz", - "integrity": "sha512-IMRc0xYuzRvtFDMuaWHyVbvM7CuAi0g3o2jaVgLDvETXPrXWAMWsHYR5niIdWBDPgGUq+soHkag1DKXyhPDB0w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-8.14.0.tgz", + "integrity": "sha512-sGiznEb3rl6pKGGUZ+JmfI7ct5cwbTQGo+IjewaTvtzfrshnryu4dZwEsjw0YHABpBA+kCz3kpRaHB7qpa67jg==", "dependencies": { "fastify-plugin": "^4.0.0", "json-schema-resolver": "^2.0.0", @@ -1790,9 +1802,9 @@ } }, "node_modules/@fastify/swagger-ui": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-1.10.1.tgz", - "integrity": "sha512-u3EJqNKvVr3X+6jY5i6pbs6/tXCrSlqc2Y+PVjnHBTOGh/d36uHMz+z4jPFy9gie2my6iHUrAdM8itlVmoUjog==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-2.1.0.tgz", + "integrity": "sha512-mu0C28kMEQDa3miE8f3LmI/OQSmqaKS3dYhZVFO5y4JdgBIPbzZj6COCoRU/P/9nu7UogzzcCJtg89wwLwKtWg==", "dependencies": { "@fastify/static": "^6.0.0", "fastify-plugin": "^4.0.0", @@ -2193,297 +2205,77 @@ } }, "node_modules/@octokit/auth-app": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-4.0.13.tgz", - "integrity": "sha512-NBQkmR/Zsc+8fWcVIFrwDgNXS7f4XDrkd9LHdi9DPQw1NdGHLviLzRO2ZBwTtepnwHXW5VTrVU9eFGijMUqllg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.3.tgz", + "integrity": "sha512-9N7IlBAKEJR3tJgPSubCxIDYGXSdc+2xbkjYpk9nCyqREnH8qEMoMhiEB1WgoA9yTFp91El92XNXAi+AjuKnfw==", "dependencies": { - "@octokit/auth-oauth-app": "^5.0.0", - "@octokit/auth-oauth-user": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", "deprecation": "^2.3.1", - "lru-cache": "^9.0.0", - "universal-github-app-jwt": "^1.1.1", + "lru-cache": "^10.0.0", + "universal-github-app-jwt": "^1.1.2", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "node": ">= 18" } }, "node_modules/@octokit/auth-app/node_modules/lru-cache": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz", - "integrity": "sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "engines": { "node": "14 || >=16.14" } }, "node_modules/@octokit/auth-oauth-app": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-5.0.6.tgz", - "integrity": "sha512-SxyfIBfeFcWd9Z/m1xa4LENTQ3l1y6Nrg31k2Dcb1jS5ov7pmwMJZ6OGX8q3K9slRgVpeAjNA1ipOAMHkieqyw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.1.tgz", + "integrity": "sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==", "dependencies": { - "@octokit/auth-oauth-device": "^4.0.0", - "@octokit/auth-oauth-user": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", + "@octokit/auth-oauth-device": "^6.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/request": "^8.0.2", + "@octokit/types": "^12.0.0", "@types/btoa-lite": "^1.0.0", "btoa-lite": "^1.0.0", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "node": ">= 18" } }, "node_modules/@octokit/auth-oauth-device": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-4.0.5.tgz", - "integrity": "sha512-XyhoWRTzf2ZX0aZ52a6Ew5S5VBAfwwx1QnC2Np6Et3MWQpZjlREIcbcvVZtkNuXp6Z9EeiSLSDUqm3C+aMEHzQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.1.tgz", + "integrity": "sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==", "dependencies": { - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", + "@octokit/oauth-methods": "^4.0.0", + "@octokit/request": "^8.0.0", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "node": ">= 18" } }, "node_modules/@octokit/auth-oauth-user": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-2.1.2.tgz", - "integrity": "sha512-kkRqNmFe7s5GQcojE3nSlF+AzYPpPv7kvP/xYEnE57584pixaFBH8Vovt+w5Y3E4zWUEOxjdLItmBTFAWECPAg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.1.tgz", + "integrity": "sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==", "dependencies": { - "@octokit/auth-oauth-device": "^4.0.0", - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", + "@octokit/auth-oauth-device": "^6.0.0", + "@octokit/oauth-methods": "^4.0.0", + "@octokit/request": "^8.0.2", + "@octokit/types": "^12.0.0", "btoa-lite": "^1.0.0", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "node": ">= 18" } }, "node_modules/@octokit/auth-token": { @@ -2495,41 +2287,15 @@ } }, "node_modules/@octokit/auth-unauthenticated": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-3.0.5.tgz", - "integrity": "sha512-yH2GPFcjrTvDWPwJWWCh0tPPtTL5SMgivgKPA+6v/XmYN6hGQkAto8JtZibSKOpf8ipmeYhLNWQ2UgW0GYILCw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", "dependencies": { - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0" + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "node": ">= 18" } }, "node_modules/@octokit/core": { @@ -2575,81 +2341,26 @@ } }, "node_modules/@octokit/oauth-authorization-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-5.0.0.tgz", - "integrity": "sha512-y1WhN+ERDZTh0qZ4SR+zotgsQUE1ysKnvBt1hvDRB2WRzYtVKQjn97HEPzoehh66Fj9LwNdlZh+p6TJatT0zzg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/oauth-methods": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-2.0.6.tgz", - "integrity": "sha512-l9Uml2iGN2aTWLZcm8hV+neBiFXAQ9+3sKiQe/sgumHlL6HDg0AQ8/l16xX/5jJvfxueqTW5CWbzd0MjnlfHZw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.0.1.tgz", + "integrity": "sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==", "dependencies": { - "@octokit/oauth-authorization-url": "^5.0.0", - "@octokit/request": "^6.2.3", - "@octokit/request-error": "^3.0.3", - "@octokit/types": "^9.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", "btoa-lite": "^1.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "node": ">= 18" } }, "node_modules/@octokit/openapi-types": { @@ -2658,35 +2369,15 @@ "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" }, "node_modules/@octokit/plugin-enterprise-compatibility": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-compatibility/-/plugin-enterprise-compatibility-1.3.0.tgz", - "integrity": "sha512-h34sMGdEOER/OKrZJ55v26ntdHb9OPfR1fwOx6Q4qYyyhWA104o11h9tFxnS/l41gED6WEI41Vu2G2zHDVC5lQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-compatibility/-/plugin-enterprise-compatibility-4.0.1.tgz", + "integrity": "sha512-d5cqeO0F/xZsTxOPOTYdw+0x8p+9GuTGGPj7oGj3y9vLluGnd7q97PTEzeJnOSERrhS4DguihQmrGu+7PhVP9Q==", "dependencies": { - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.0.3" - } - }, - "node_modules/@octokit/plugin-enterprise-compatibility/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/@octokit/plugin-enterprise-compatibility/node_modules/@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "node_modules/@octokit/plugin-enterprise-compatibility/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/plugin-paginate-rest": { @@ -2729,25 +2420,34 @@ } }, "node_modules/@octokit/plugin-retry": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz", - "integrity": "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", "dependencies": { - "@octokit/types": "^6.0.3", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" } }, - "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "node_modules/@octokit/plugin-throttling": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.1.3.tgz", + "integrity": "sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==", "dependencies": { - "@octokit/openapi-types": "^12.11.0" + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" } }, "node_modules/@octokit/request": { @@ -2800,53 +2500,36 @@ } }, "node_modules/@octokit/webhooks": { - "version": "9.26.3", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-9.26.3.tgz", - "integrity": "sha512-DLGk+gzeVq5oK89Bo601txYmyrelMQ7Fi5EnjHE0Xs8CWicy2xkmnJMKptKJrBJpstqbd/9oeDFi/Zj2pudBDQ==", + "version": "12.0.11", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.0.11.tgz", + "integrity": "sha512-YEQOb7v0TZ662nh5jsbY1CMgJyMajCEagKrHWC30LTCwCtnuIrLtEpE20vq4AtH0SuZI90+PtV66/Bnnw0jkvg==", "dependencies": { - "@octokit/request-error": "^2.0.2", - "@octokit/webhooks-methods": "^2.0.0", - "@octokit/webhooks-types": "5.8.0", + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.0.0", + "@octokit/webhooks-types": "7.1.0", "aggregate-error": "^3.1.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/webhooks-methods": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-2.0.0.tgz", - "integrity": "sha512-35cfQ4YWlnZnmZKmIxlGPUPLtbkF8lr/A/1Sk1eC0ddLMwQN06dOuLc+dI3YLQS+T+MoNt3DIQ0NynwgKPilig==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.0.0.tgz", + "integrity": "sha512-M8mwmTXp+VeolOS/kfRvsDdW+IO0qJ8kYodM/sAysk093q6ApgmBXwK1ZlUvAwXVrp/YVHp6aArj4auAxUAOFw==", + "engines": { + "node": ">= 18" + } }, "node_modules/@octokit/webhooks-types": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.3.1.tgz", "integrity": "sha512-u6355ZsZnHwmxen30SrqnYb1pXieBFkYgkNzt+Ed4Ao5tupN1OErHfzwiV6hq6duGkDAYASbq7/uVJQ69PjLEg==" }, - "node_modules/@octokit/webhooks/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/@octokit/webhooks/node_modules/@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "node_modules/@octokit/webhooks/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, "node_modules/@octokit/webhooks/node_modules/@octokit/webhooks-types": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-5.8.0.tgz", - "integrity": "sha512-8adktjIb76A7viIdayQSFuBEwOzwhDC+9yxZpKNHjfzrlostHCw0/N7JWpWMObfElwvJMk2fY2l1noENCk9wmw==" + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz", + "integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w==" }, "node_modules/@phc/format": { "version": "1.0.0", @@ -2884,15 +2567,17 @@ "integrity": "sha512-yVgyCdTyooGX6+czDLkJahEcwgBWZsKH9xbjvjDNVFjY3QtiI/tHRiB3zjgJCQMZehXxv2CFHZQSpWRXdr6CeQ==" }, "node_modules/@probot/octokit-plugin-config": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@probot/octokit-plugin-config/-/octokit-plugin-config-1.1.6.tgz", - "integrity": "sha512-L29wmnFvilzSfWn9tUgItxdLv0LJh2ICjma3FmLr80Spu3wZ9nHyRrKMo9R5/K2m7VuWmgoKnkgRt2zPzAQBEQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@probot/octokit-plugin-config/-/octokit-plugin-config-2.0.1.tgz", + "integrity": "sha512-aWQYzPY2xiKscTVTKveghtbglqZ+W4eBLIdK1C/cNiFIofy3AxKogWgEZj29PjIe5ZRYx0sRHAPc/pkcXyOmTQ==", "dependencies": { - "@types/js-yaml": "^4.0.5", "js-yaml": "^4.1.0" }, + "engines": { + "node": ">=18" + }, "peerDependencies": { - "@octokit/core": ">=3" + "@octokit/core": ">=5" } }, "node_modules/@probot/pino": { @@ -4207,6 +3892,12 @@ "@types/ms": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -4234,25 +3925,12 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, - "node_modules/@types/ioredis": { - "version": "4.28.10", - "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", - "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/jmespath": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz", "integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==", "dev": true }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4460,51 +4138,6 @@ "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", "dev": true }, - "node_modules/@types/pino": { - "version": "6.3.12", - "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.12.tgz", - "integrity": "sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==", - "dependencies": { - "@types/node": "*", - "@types/pino-pretty": "*", - "@types/pino-std-serializers": "*", - "sonic-boom": "^2.1.0" - } - }, - "node_modules/@types/pino-http": { - "version": "5.8.4", - "resolved": "https://registry.npmjs.org/@types/pino-http/-/pino-http-5.8.4.tgz", - "integrity": "sha512-UTYBQ2acmJ2eK0w58vVtgZ9RAicFFndfrnWC1w5cBTf8zwn/HEy8O+H7psc03UZgTzHmlcuX8VkPRnRDEj+FUQ==", - "dependencies": { - "@types/pino": "6.3" - } - }, - "node_modules/@types/pino-pretty": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/pino-pretty/-/pino-pretty-5.0.0.tgz", - "integrity": "sha512-N1uzqSzioqz8R3AkDbSJwcfDWeI3YMPNapSQQhnB2ISU4NYgUIcAh+hYT5ygqBM+klX4htpEhXMmoJv3J7GrdA==", - "deprecated": "This is a stub types definition. pino-pretty provides its own type definitions, so you do not need this installed.", - "dependencies": { - "pino-pretty": "*" - } - }, - "node_modules/@types/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-gXfUZx2xIBbFYozGms53fT0nvkacx/+62c8iTxrEqH5PkIGAQvDbXg2774VWOycMPbqn5YJBQ3BMsg4Li3dWbg==", - "deprecated": "This is a stub types definition. pino-std-serializers provides its own type definitions, so you do not need this installed.", - "dependencies": { - "pino-std-serializers": "*" - } - }, - "node_modules/@types/pino/node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -4927,13 +4560,13 @@ "dev": true }, "node_modules/@vitest/expect": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.0.4.tgz", - "integrity": "sha512-/NRN9N88qjg3dkhmFcCBwhn/Ie4h064pY3iv7WLRsDJW7dXnEgeoa8W9zy7gIPluhz6CkgqiB3HmpIXgmEY5dQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", + "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", "dev": true, "dependencies": { - "@vitest/spy": "1.0.4", - "@vitest/utils": "1.0.4", + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", "chai": "^4.3.10" }, "funding": { @@ -4941,12 +4574,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.0.4.tgz", - "integrity": "sha512-rhOQ9FZTEkV41JWXozFM8YgOqaG9zA7QXbhg5gy6mFOVqh4PcupirIJ+wN7QjeJt8S8nJRYuZH1OjJjsbxAXTQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", + "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", "dev": true, "dependencies": { - "@vitest/utils": "1.0.4", + "@vitest/utils": "1.2.2", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -4982,9 +4615,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.0.4.tgz", - "integrity": "sha512-vkfXUrNyNRA/Gzsp2lpyJxh94vU2OHT1amoD6WuvUAA12n32xeVZQ0KjjQIf8F6u7bcq2A2k969fMVxEsxeKYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", + "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -4996,9 +4629,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.0.4.tgz", - "integrity": "sha512-9ojTFRL1AJVh0hvfzAQpm0QS6xIS+1HFIw94kl/1ucTfGCaj1LV/iuJU4Y6cdR03EzPDygxTHwE1JOm+5RCcvA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", + "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -5008,12 +4641,13 @@ } }, "node_modules/@vitest/utils": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.0.4.tgz", - "integrity": "sha512-gsswWDXxtt0QvtK/y/LWukN7sGMYmnCcv1qv05CsY6cU/Y1zpGX1QuvLs+GO1inczpE6Owixeel3ShkjhYtGfA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", + "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" }, @@ -5084,9 +4718,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, "engines": { "node": ">=0.4.0" @@ -5171,7 +4805,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -5536,9 +5169,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/aws-sdk": { - "version": "2.1532.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1532.0.tgz", - "integrity": "sha512-4QVQs01LEAxo7UpSHlq/HaO+SJ1WrYF8W1otO2WhKpVRYXkSxXIgZgfYaK+sQ762XTtB6tSuD2ZS2HGsKNXVLw==", + "version": "2.1545.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1545.0.tgz", + "integrity": "sha512-iDUv6ksG7lTA0l/HlOgYdO6vfYFA1D2/JzAEXSdgKY0C901WgJqBtfs2CncOkCgDe2CjmlMuqciBzAfxCIiKFA==", "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -5549,7 +5182,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.5.0" + "xml2js": "0.6.2" }, "engines": { "node": ">= 10.0.0" @@ -5596,12 +5229,32 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "node_modules/aws-sdk/node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dependencies": { - "follow-redirects": "^1.15.0", + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/aws-sdk/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/axios": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz", + "integrity": "sha512-heJnIs6N4aa1eSthhN9M5ioILu8Wi8vmQW9iHQ9NUvfkJb0lEEDUiIdQNAuBtfUt3FxReaKdpQA5DbmMOqzF/A==", + "dependencies": { + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -5676,6 +5329,29 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -5796,15 +5472,15 @@ } }, "node_modules/bullmq": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.1.1.tgz", - "integrity": "sha512-j3zbNEQWsyHjpqGWiem2XBfmxAjYcArbwsmGlkM1E9MAVcrqB5hQUsXmyy9gEBAdL+PVotMICr7xTquR4Y2sKQ==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.1.6.tgz", + "integrity": "sha512-VkLfig+xm4U3hc4QChzuuAy0NGQ9dfPB8o54hmcZHCX9ofp0Zn6bEY+W3Ytkk76eYwPAgXfywDBlAb2Unjl1Rg==", "dependencies": { "cron-parser": "^4.6.0", "glob": "^8.0.3", "ioredis": "^5.3.2", "lodash": "^4.17.21", - "msgpackr": "^1.6.2", + "msgpackr": "^1.10.1", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", @@ -5874,9 +5550,9 @@ } }, "node_modules/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -5891,6 +5567,17 @@ "node": ">=4" } }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -5955,6 +5642,31 @@ "node": ">=6" } }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -6000,7 +5712,8 @@ "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -6319,8 +6032,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -6335,6 +6047,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -6996,18 +6713,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -7041,6 +6746,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7123,19 +6837,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express-handlebars": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-6.0.7.tgz", - "integrity": "sha512-iYeMFpc/hMD+E6FNAZA5fgWeXnXr4rslOSPkeEV6TwdmpJ5lEXuWX0u9vFYs31P2MURctQq2batR09oeNj0LIg==", - "dependencies": { - "glob": "^8.1.0", - "graceful-fs": "^4.2.10", - "handlebars": "^4.7.7" - }, - "engines": { - "node": ">=v12.22.9" - } - }, "node_modules/express/node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -7162,7 +6863,8 @@ "node_modules/fast-copy": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", - "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==", + "dev": true }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", @@ -7248,19 +6950,6 @@ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", - "dependencies": { - "punycode": "^1.3.2" - } - }, - "node_modules/fast-url-parser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - }, "node_modules/fast-xml-parser": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", @@ -7283,9 +6972,19 @@ } }, "node_modules/fastify": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.24.3.tgz", - "integrity": "sha512-6HHJ+R2x2LS3y1PqxnwEIjOTZxFl+8h4kSC/TuDPXtA+v2JnV9yEtOsNSKK1RMD7sIR2y1ZsA4BEFaid/cK5pg==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.0.tgz", + "integrity": "sha512-Fq/7ziWKc6pYLYLIlCRaqJqEVTIZ5tZYfcW/mDK2AQ9v/sqjGFpj0On0/7hU50kbPVjLO4de+larPA1WwPZSfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "dependencies": { "@fastify/ajv-compiler": "^3.5.0", "@fastify/error": "^3.4.0", @@ -7294,10 +6993,10 @@ "avvio": "^8.2.1", "fast-content-type-parse": "^1.1.0", "fast-json-stringify": "^5.8.0", - "find-my-way": "^7.7.0", + "find-my-way": "^8.0.0", "light-my-request": "^5.11.0", - "pino": "^8.16.0", - "process-warning": "^2.2.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", "proxy-addr": "^2.0.7", "rfdc": "^1.3.0", "secure-json-parse": "^2.7.0", @@ -7310,6 +7009,11 @@ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" }, + "node_modules/fastify/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -7373,9 +7077,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/find-my-way": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", - "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", @@ -7450,11 +7154,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" - }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -7685,6 +7384,14 @@ "is-property": "^1.0.2" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -8026,6 +7733,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dev": true, "dependencies": { "glob": "^8.0.0", "readable-stream": "^3.6.0" @@ -8035,6 +7743,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8421,6 +8130,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -8466,14 +8186,6 @@ "node": ">=8" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -8574,6 +8286,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -9008,11 +8731,6 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -9070,6 +8788,21 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "dev": true }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -9109,9 +8842,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -9253,7 +8986,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "engines": { "node": ">=6" } @@ -9395,9 +9127,9 @@ } }, "node_modules/mysql2": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.5.tgz", - "integrity": "sha512-pS/KqIb0xlXmtmqEuTvBXTmLoQ5LmAz5NW/r8UyQ1ldvnprNEj3P9GbmuQQ2J0A4LO+ynotGi6TbscPa8OUb+w==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz", + "integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -9777,38 +9509,20 @@ "dev": true }, "node_modules/octokit-auth-probot": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-1.2.9.tgz", - "integrity": "sha512-mMjw6Y760EwJnW2tSVooJK8BMdsG6D40SoCclnefVf/5yWjaNVquEu8NREBVWb60OwbpnMEz4vREXHB5xdMFYQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-2.0.0.tgz", + "integrity": "sha512-bxidVIyxYJ+hWkG24pchPrN6mJdQrklZ2Acu+oGmZlh9aRONsIrw0KNW5W7QC2VlkxsFQwb9lnV+vH0BcEhnLQ==", "dependencies": { - "@octokit/auth-app": "^4.0.2", - "@octokit/auth-token": "^3.0.0", - "@octokit/auth-unauthenticated": "^3.0.0", - "@octokit/types": "^8.0.0" + "@octokit/auth-app": "^6.0.1", + "@octokit/auth-token": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.1", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "@octokit/core": ">=3.2" - } - }, - "node_modules/octokit-auth-probot/node_modules/@octokit/auth-token": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", - "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==", - "engines": { - "node": ">= 14" - } - }, - "node_modules/octokit-auth-probot/node_modules/@octokit/openapi-types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", - "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==" - }, - "node_modules/octokit-auth-probot/node_modules/@octokit/types": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.2.1.tgz", - "integrity": "sha512-8oWMUji8be66q2B9PmEIUyQm00VPDPun07umUWSaCwxmeaquFBro4Hcc3ruVoDo3zkQyZBlRvhIMEYS3pBhanw==", - "dependencies": { - "@octokit/openapi-types": "^14.0.0" + "@octokit/core": ">=5" } }, "node_modules/on-exit-leak-free": { @@ -9842,7 +9556,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -9875,6 +9588,28 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9904,14 +9639,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "engines": { - "node": ">=6" - } - }, "node_modules/p-throttle": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.1.0.tgz", @@ -10258,16 +9985,16 @@ } }, "node_modules/pino": { - "version": "8.16.2", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.2.tgz", - "integrity": "sha512-2advCDGVEvkKu9TTVSa/kWW7Z3htI/sBKEZpqiHk6ive0i/7f5b1rsU8jn0aimxqfnSz5bj/nOYkwhBUn5xxvg==", + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.17.2.tgz", + "integrity": "sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", - "process-warning": "^2.0.0", + "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", @@ -10288,60 +10015,26 @@ } }, "node_modules/pino-http": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-5.8.0.tgz", - "integrity": "sha512-YwXiyRb9y0WCD1P9PcxuJuh3Dc5qmXde/paJE86UGYRdiFOi828hR9iUGmk5gaw6NBT9gLtKANOHFimvh19U5w==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.6.1.tgz", + "integrity": "sha512-J0hiJgUExtBXP2BjrK4VB305tHXS31sCmWJ9XJo2wPkLHa1NFPuW4V9wjG27PAc2fmBCigiNhQKpvrx+kntBPA==", "dependencies": { - "fast-url-parser": "^1.1.3", - "pino": "^6.13.0", - "pino-std-serializers": "^4.0.0" + "get-caller-file": "^2.0.5", + "pino": "^8.17.1", + "pino-std-serializers": "^6.2.2", + "process-warning": "^3.0.0" } }, - "node_modules/pino-http/node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", - "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-http/node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" - }, - "node_modules/pino-http/node_modules/pino/node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" - }, "node_modules/pino-http/node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" - }, - "node_modules/pino-http/node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, "node_modules/pino-pretty": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.3.tgz", "integrity": "sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==", + "dev": true, "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", @@ -10367,6 +10060,11 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, + "node_modules/pino/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -10665,413 +10363,59 @@ } }, "node_modules/probot": { - "version": "12.3.3", - "resolved": "https://registry.npmjs.org/probot/-/probot-12.3.3.tgz", - "integrity": "sha512-cdtKd+xISzi8sw6++BYBXleRknCA6hqUMoHj/sJqQBrjbNxQLhfeFCq9O2d0Z4eShsy5YFRR3MWwDKJ9uAE0CA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/probot/-/probot-13.0.0.tgz", + "integrity": "sha512-3ht9kAJ+ISjLyWLLCKVdrLE5xs/x+zUx07J5kYTxAyIxUvwF6Acr8xT5fiNihbBHAsEl4+A4CMYZQvZ5hx5bgw==", "dependencies": { - "@octokit/core": "^3.2.4", - "@octokit/plugin-enterprise-compatibility": "^1.2.8", - "@octokit/plugin-paginate-rest": "^2.6.2", - "@octokit/plugin-rest-endpoint-methods": "^5.0.1", - "@octokit/plugin-retry": "^3.0.6", - "@octokit/plugin-throttling": "^3.3.4", - "@octokit/types": "^8.0.0", - "@octokit/webhooks": "^9.26.3", - "@probot/get-private-key": "^1.1.0", - "@probot/octokit-plugin-config": "^1.0.0", - "@probot/pino": "^2.2.0", - "@types/express": "^4.17.9", - "@types/ioredis": "^4.27.1", - "@types/pino": "^6.3.4", - "@types/pino-http": "^5.0.6", - "commander": "^6.2.0", - "deepmerge": "^4.2.2", - "deprecation": "^2.3.1", - "dotenv": "^8.2.0", + "@octokit/core": "^5.0.2", + "@octokit/plugin-enterprise-compatibility": "^4.0.1", + "@octokit/plugin-paginate-rest": "^9.1.4", + "@octokit/plugin-rest-endpoint-methods": "^10.1.5", + "@octokit/plugin-retry": "^6.0.1", + "@octokit/plugin-throttling": "^8.1.3", + "@octokit/request": "^8.1.6", + "@octokit/types": "^12.3.0", + "@octokit/webhooks": "^12.0.10", + "@probot/get-private-key": "^1.1.2", + "@probot/octokit-plugin-config": "^2.0.1", + "@probot/pino": "^2.3.5", + "@types/express": "^4.17.21", + "commander": "^11.1.0", + "deepmerge": "^4.3.1", + "dotenv": "^16.3.1", "eventsource": "^2.0.2", - "express": "^4.17.1", - "express-handlebars": "^6.0.3", - "ioredis": "^4.27.8", - "js-yaml": "^3.14.1", - "lru-cache": "^6.0.0", - "octokit-auth-probot": "^1.2.2", - "pino": "^6.7.0", - "pino-http": "^5.3.0", + "express": "^4.18.2", + "ioredis": "^5.3.2", + "js-yaml": "^4.1.0", + "lru-cache": "^10.0.3", + "octokit-auth-probot": "^2.0.0", + "pino": "^8.16.1", + "pino-http": "^8.5.1", "pkg-conf": "^3.1.0", - "resolve": "^1.19.0", - "semver": "^7.3.4", - "update-dotenv": "^1.1.1", - "uuid": "^8.3.2" + "resolve": "^1.22.8", + "update-dotenv": "^1.1.1" }, "bin": { "probot": "bin/probot.js" }, "engines": { - "node": ">=10.21" - } - }, - "node_modules/probot/node_modules/@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "dependencies": { - "@octokit/types": "^6.0.3" - } - }, - "node_modules/probot/node_modules/@octokit/auth-token/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/auth-token/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", - "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", - "dependencies": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/probot/node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "dependencies": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/probot/node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", - "dependencies": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/probot/node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/openapi-types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", - "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==" - }, - "node_modules/probot/node_modules/@octokit/plugin-paginate-rest": { - "version": "2.21.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", - "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", - "dependencies": { - "@octokit/types": "^6.40.0" - }, - "peerDependencies": { - "@octokit/core": ">=2" - } - }, - "node_modules/probot/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", - "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", - "dependencies": { - "@octokit/types": "^6.39.0", - "deprecation": "^2.3.1" - }, - "peerDependencies": { - "@octokit/core": ">=3" - } - }, - "node_modules/probot/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/plugin-throttling": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-3.7.0.tgz", - "integrity": "sha512-qrKT1Yl/KuwGSC6/oHpLBot3ooC9rq0/ryDYBCpkRtoj+R8T47xTMDT6Tk2CxWopFota/8Pi/2SqArqwC0JPow==", - "dependencies": { - "@octokit/types": "^6.0.1", - "bottleneck": "^2.15.3" - }, - "peerDependencies": { - "@octokit/core": "^3.5.0" - } - }, - "node_modules/probot/node_modules/@octokit/plugin-throttling/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/plugin-throttling/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/request": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", - "dependencies": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/probot/node_modules/@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "node_modules/probot/node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/probot/node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/probot/node_modules/@octokit/types": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.2.1.tgz", - "integrity": "sha512-8oWMUji8be66q2B9PmEIUyQm00VPDPun07umUWSaCwxmeaquFBro4Hcc3ruVoDo3zkQyZBlRvhIMEYS3pBhanw==", - "dependencies": { - "@octokit/openapi-types": "^14.0.0" - } - }, - "node_modules/probot/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" + "node": ">=18" } }, "node_modules/probot/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "engines": { - "node": ">= 6" + "node": ">=16" } }, - "node_modules/probot/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, + "node_modules/probot/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/probot/node_modules/denque": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", - "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/probot/node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - }, - "node_modules/probot/node_modules/ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", - "dependencies": { - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", - "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", - "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/probot/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/probot/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/probot/node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", - "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/probot/node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" - }, - "node_modules/probot/node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" - }, - "node_modules/probot/node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } - }, - "node_modules/probot/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" + "node": "14 || >=16.14" } }, "node_modules/process": { @@ -11319,11 +10663,6 @@ "node": ">= 10.13.0" } }, - "node_modules/redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" - }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -11402,6 +10741,21 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ret": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", @@ -11891,11 +11245,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -11929,6 +11278,20 @@ "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", "dev": true }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", @@ -11942,6 +11305,22 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", @@ -12033,7 +11412,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -12286,18 +11664,18 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", - "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, "engines": { "node": ">=14.0.0" @@ -13430,9 +12808,9 @@ } }, "node_modules/vite-node": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.0.4.tgz", - "integrity": "sha512-9xQQtHdsz5Qn8hqbV7UKqkm8YkJhzT/zr41Dmt5N7AlD8hJXw/Z7y0QiD5I8lnTthV9Rvcvi0QW7PI0Fq83ZPg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", + "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -13906,17 +13284,17 @@ } }, "node_modules/vitest": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.0.4.tgz", - "integrity": "sha512-s1GQHp/UOeWEo4+aXDOeFBJwFzL6mjycbQwwKWX2QcYfh/7tIerS59hWQ20mxzupTJluA2SdwiBuWwQHH67ckg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", + "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", "dev": true, "dependencies": { - "@vitest/expect": "1.0.4", - "@vitest/runner": "1.0.4", - "@vitest/snapshot": "1.0.4", - "@vitest/spy": "1.0.4", - "@vitest/utils": "1.0.4", - "acorn-walk": "^8.3.0", + "@vitest/expect": "1.2.2", + "@vitest/runner": "1.2.2", + "@vitest/snapshot": "1.2.2", + "@vitest/spy": "1.2.2", + "@vitest/utils": "1.2.2", + "acorn-walk": "^8.3.2", "cac": "^6.7.14", "chai": "^4.3.10", "debug": "^4.3.4", @@ -13928,9 +13306,9 @@ "std-env": "^3.5.0", "strip-literal": "^1.3.0", "tinybench": "^2.5.1", - "tinypool": "^0.8.1", + "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.0.4", + "vite-node": "1.2.2", "why-is-node-running": "^2.2.2" }, "bin": { @@ -14449,9 +13827,9 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.0.tgz", - "integrity": "sha512-XQr8EwxPMzJGhoR+d/nRFWdi15VaZ+R5Uhssm+Xx5yS30xCpuutfKRm4rerE0SK9j2dWB5Z3FvDD0w8WMVGzkA==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.4.tgz", + "integrity": "sha512-2Ed5dJ+n/O3cU383xSY28cuVi0BCQhF8nYqWU5paEpl7fVdqdAmiLdqLyfblbNdfOFwFfi/mqU4O1pwc60iBhQ==", "peerDependencies": { "zod": "^3.22.4" } diff --git a/backend/package.json b/backend/package.json index 6b4d32317..0b79939f0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,8 +24,8 @@ "migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", "migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback", "seed:new": "tsx ./scripts/create-seed-file.ts", - "seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", - "db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run" + "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", + "db:reset": "npm run migration:rollback -- --all && npm run migration:latest" }, "keywords": [], "author": "", @@ -67,10 +67,10 @@ "tsx": "^4.4.0", "typescript": "^5.3.2", "vite-tsconfig-paths": "^4.2.2", - "vitest": "^1.0.4" + "vitest": "^1.2.2" }, "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.485.0", + "@aws-sdk/client-secrets-manager": "^3.502.0", "@casl/ability": "^6.5.0", "@fastify/cookie": "^9.2.0", "@fastify/cors": "^8.4.1", @@ -80,8 +80,8 @@ "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", "@fastify/session": "^10.7.0", - "@fastify/swagger": "^8.12.0", - "@fastify/swagger-ui": "^1.10.1", + "@fastify/swagger": "^8.14.0", + "@fastify/swagger-ui": "^2.1.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", @@ -90,13 +90,13 @@ "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", - "aws-sdk": "^2.1532.0", - "axios": "^1.6.2", + "aws-sdk": "^2.1545.0", + "axios": "^1.6.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", - "bullmq": "^5.1.1", + "bullmq": "^5.1.6", "dotenv": "^16.3.1", - "fastify": "^4.24.3", + "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", "handlebars": "^4.7.8", "ioredis": "^5.3.2", @@ -106,10 +106,11 @@ "knex": "^3.0.1", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", - "mysql2": "^3.6.5", + "mysql2": "^3.9.1", "nanoid": "^5.0.4", "node-cache": "^5.1.2", - "nodemailer": "^6.9.7", + "nodemailer": "^6.9.9", + "ora": "^7.0.1", "passport-github": "^1.1.0", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", @@ -117,12 +118,12 @@ "picomatch": "^3.0.1", "pino": "^8.16.2", "posthog-node": "^3.6.0", - "probot": "^12.3.3", + "probot": "^13.0.0", "smee-client": "^2.0.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "uuid": "^9.0.1", "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.0" + "zod-to-json-schema": "^3.22.4" } -} \ No newline at end of file +} diff --git a/backend/scripts/create-seed-file.ts b/backend/scripts/create-seed-file.ts index 25faf94c3..c79ea0846 100644 --- a/backend/scripts/create-seed-file.ts +++ b/backend/scripts/create-seed-file.ts @@ -7,11 +7,10 @@ import promptSync from "prompt-sync"; const prompt = promptSync({ sigint: true }); const migrationName = prompt("Enter name for seedfile: "); -const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1; +const fileCounter = readdirSync(path.join(__dirname, "../src/db/seeds")).length || 1; execSync( - `npx knex seed:make --knexfile ${path.join( - __dirname, - "../src/db/knexfile.ts" - )} -x ts ${fileCounter}-${migrationName}`, + `npx knex seed:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${ + fileCounter + 1 + }-${migrationName}`, { stdio: "inherit" } ); diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index ef850bb8c..bd7a09d86 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -6,6 +6,7 @@ import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; +import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service"; import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service"; @@ -105,6 +106,7 @@ declare module "fastify" { secretRotation: TSecretRotationServiceFactory; snapshot: TSecretSnapshotServiceFactory; saml: TSamlConfigServiceFactory; + scim: TScimServiceFactory; auditLog: TAuditLogServiceFactory; secretScanning: TSecretScanningServiceFactory; license: TLicenseServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 5bf563810..5b5961558 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -83,6 +83,9 @@ import { TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, + TScimTokens, + TScimTokensInsert, + TScimTokensUpdate, TSecretApprovalPolicies, TSecretApprovalPoliciesApprovers, TSecretApprovalPoliciesApproversInsert, @@ -262,6 +265,7 @@ declare module "knex/types/tables" { TIdentityProjectMembershipsInsert, TIdentityProjectMembershipsUpdate >; + [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, TSecretApprovalPoliciesInsert, diff --git a/backend/src/db/knexfile.ts b/backend/src/db/knexfile.ts index 73eb507f4..b81285d22 100644 --- a/backend/src/db/knexfile.ts +++ b/backend/src/db/knexfile.ts @@ -10,6 +10,10 @@ dotenv.config({ path: path.join(__dirname, "../../../.env.migration"), debug: true }); +dotenv.config({ + path: path.join(__dirname, "../../../.env"), + debug: true +}); export default { development: { client: "postgres", diff --git a/backend/src/db/migrations/20240208234120_scim-token.ts b/backend/src/db/migrations/20240208234120_scim-token.ts new file mode 100644 index 000000000..28121362a --- /dev/null +++ b/backend/src/db/migrations/20240208234120_scim-token.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.ScimToken))) { + await knex.schema.createTable(TableName.ScimToken, (t) => { + t.string("id", 36).primary().defaultTo(knex.fn.uuid()); + t.bigInteger("ttlDays").defaultTo(365).notNullable(); + t.string("description").notNullable(); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("scimEnabled").defaultTo(false); + }); + + await createOnUpdateTrigger(knex, TableName.ScimToken); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ScimToken); + await dropOnUpdateTrigger(knex, TableName.ScimToken); + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("scimEnabled"); + }); +} diff --git a/backend/src/db/migrations/20240216154123_ghost_users.ts b/backend/src/db/migrations/20240216154123_ghost_users.ts new file mode 100644 index 000000000..d0a840ee2 --- /dev/null +++ b/backend/src/db/migrations/20240216154123_ghost_users.ts @@ -0,0 +1,39 @@ +import { Knex } from "knex"; + +import { ProjectVersion, TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost"); + const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version"); + + if (!hasGhostUserColumn) { + await knex.schema.alterTable(TableName.Users, (t) => { + t.boolean("isGhost").defaultTo(false).notNullable(); + }); + } + + if (!hasProjectVersionColumn) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.integer("version").defaultTo(ProjectVersion.V1).notNullable(); + t.string("upgradeStatus").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost"); + const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version"); + + if (hasGhostUserColumn) { + await knex.schema.alterTable(TableName.Users, (t) => { + t.dropColumn("isGhost"); + }); + } + + if (hasProjectVersionColumn) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("version"); + t.dropColumn("upgradeStatus"); + }); + } +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 62b01ebd2..b330c90b7 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -26,6 +26,7 @@ export * from "./project-memberships"; export * from "./project-roles"; export * from "./projects"; export * from "./saml-configs"; +export * from "./scim-tokens"; export * from "./secret-approval-policies"; export * from "./secret-approval-policies-approvers"; export * from "./secret-approval-request-secret-tags"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 4ef943bbe..abfb67949 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -40,6 +40,7 @@ export enum TableName { IdentityUaClientSecret = "identity_ua_client_secrets", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", + ScimToken = "scim_tokens", SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalRequest = "secret_approval_requests", @@ -111,6 +112,17 @@ export enum SecretType { Personal = "personal" } +export enum ProjectVersion { + V1 = 1, + V2 = 2 +} + +export enum ProjectUpgradeStatus { + InProgress = "IN_PROGRESS", + // Completed -> Will be null if completed. So a completed status is not needed + Failed = "FAILED" +} + export enum IdentityAuthMethod { Univeral = "universal-auth" } diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 087a1b7e0..a91b93ea5 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -14,7 +14,8 @@ export const OrganizationsSchema = z.object({ slug: z.string(), createdAt: z.date(), updatedAt: z.date(), - authEnforced: z.boolean().default(false).nullable().optional() + authEnforced: z.boolean().default(false).nullable().optional(), + scimEnabled: z.boolean().default(false).nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 296fa421e..3834d6d58 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -14,7 +14,9 @@ export const ProjectsSchema = z.object({ autoCapitalization: z.boolean().default(true).nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + version: z.number().default(1), + upgradeStatus: z.string().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/db/schemas/scim-tokens.ts b/backend/src/db/schemas/scim-tokens.ts new file mode 100644 index 000000000..593e4f7d9 --- /dev/null +++ b/backend/src/db/schemas/scim-tokens.ts @@ -0,0 +1,21 @@ +// 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 ScimTokensSchema = z.object({ + id: z.string(), + ttlDays: z.coerce.number().default(365), + description: z.string(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TScimTokens = z.infer; +export type TScimTokensInsert = Omit; +export type TScimTokensUpdate = Partial>; diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index 4a29de510..cdbb5607a 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -19,7 +19,8 @@ export const UsersSchema = z.object({ mfaMethods: z.string().array().nullable().optional(), devices: z.unknown().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + isGhost: z.boolean().default(false) }); export type TUsers = z.infer; diff --git a/backend/src/db/seed-data.ts b/backend/src/db/seed-data.ts index bb57d5bb4..6406e44ab 100644 --- a/backend/src/db/seed-data.ts +++ b/backend/src/db/seed-data.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-mutable-exports */ import crypto from "node:crypto"; import argon2, { argon2id } from "argon2"; @@ -6,17 +7,21 @@ import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; import { + decryptAsymmetric, // decryptAsymmetric, - decryptSymmetric, + decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, - encryptSymmetric + encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { TUserEncryptionKeys } from "./schemas"; +import { TSecrets, TUserEncryptionKeys } from "./schemas"; + +export let userPrivateKey: string | undefined; +export let userPublicKey: string | undefined; export const seedData1 = { id: "3dafd81d-4388-432b-a4c5-f735616868c1", - email: "test@localhost.local", + email: process.env.TEST_USER_EMAIL || "test@localhost.local", password: process.env.TEST_USER_PASSWORD || "testInfisical@1", organization: { id: "180870b7-f464-4740-8ffe-9d11c9245ea7", @@ -31,8 +36,22 @@ export const seedData1 = { name: "Development", slug: "dev" }, + machineIdentity: { + id: "88fa7aed-9288-401e-a4c9-fa9430be62a0", + name: "mac1", + clientCredentials: { + id: "3f6135db-f237-421d-af66-a8f4e80d443b", + secret: "da35a5a5a7b57f977a9a73394506e878a7175d06606df43dc93e1472b10cf339" + } + }, token: { id: "a9dfafba-a3b7-42e3-8618-91abb702fd36" + }, + + // We set these values during user creation, and later re-use them during project seeding. + encryptionKeys: { + publicKey: "", + privateKey: "" } }; @@ -73,7 +92,7 @@ export const generateUserSrpKeys = async (password: string) => { ciphertext: encryptedPrivateKey, iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag - } = encryptSymmetric(privateKey, key.toString("base64")); + } = encryptSymmetric128BitHexKeyUTF8(privateKey, key); // create the protected key by encrypting the symmetric key // [key] with the derived key @@ -81,7 +100,7 @@ export const generateUserSrpKeys = async (password: string) => { ciphertext: protectedKey, iv: protectedKeyIV, tag: protectedKeyTag - } = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64")); + } = encryptSymmetric128BitHexKeyUTF8(key.toString("hex"), derivedKey); return { protectedKey, @@ -107,32 +126,102 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK raw: true }); if (!derivedKey) throw new Error("Failed to derive key from password"); - const key = decryptSymmetric({ + + const key = decryptSymmetric128BitHexKeyUTF8({ ciphertext: user.protectedKey as string, iv: user.protectedKeyIV as string, tag: user.protectedKeyTag as string, - key: derivedKey.toString("base64") + key: derivedKey }); - const privateKey = decryptSymmetric({ + + const privateKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: user.encryptedPrivateKey, iv: user.iv, tag: user.tag, - key + key: Buffer.from(key, "hex") }); return privateKey; }; -export const buildUserProjectKey = async (privateKey: string, publickey: string) => { +export const buildUserProjectKey = (privateKey: string, publickey: string) => { const randomBytes = crypto.randomBytes(16).toString("hex"); const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey); return { nonce, ciphertext }; }; -// export const getUserProjectKey = async (privateKey: string) => { -// const key = decryptAsymmetric({ -// ciphertext: decryptFileKey.encryptedKey, -// nonce: decryptFileKey.nonce, -// publicKey: decryptFileKey.sender.publicKey, -// privateKey: PRIVATE_KEY -// }); -// }; +export const getUserProjectKey = async (privateKey: string, ciphertext: string, nonce: string, publicKey: string) => { + return decryptAsymmetric({ + ciphertext, + nonce, + publicKey, + privateKey + }); +}; + +export const encryptSecret = (encKey: string, key: string, value?: string, comment?: string) => { + // encrypt key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encryptSymmetric128BitHexKeyUTF8(key, encKey); + + // encrypt value + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encryptSymmetric128BitHexKeyUTF8(value ?? "", encKey); + + // encrypt comment + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encryptSymmetric128BitHexKeyUTF8(comment ?? "", encKey); + + return { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + }; +}; + +export const decryptSecret = (decryptKey: string, encSecret: TSecrets) => { + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + key: decryptKey, + ciphertext: encSecret.secretKeyCiphertext, + tag: encSecret.secretKeyTag, + iv: encSecret.secretKeyIV + }); + + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + key: decryptKey, + ciphertext: encSecret.secretValueCiphertext, + tag: encSecret.secretValueTag, + iv: encSecret.secretValueIV + }); + + const secretComment = + encSecret.secretCommentIV && encSecret.secretCommentTag && encSecret.secretCommentCiphertext + ? decryptSymmetric128BitHexKeyUTF8({ + key: decryptKey, + ciphertext: encSecret.secretCommentCiphertext, + tag: encSecret.secretCommentTag, + iv: encSecret.secretCommentIV + }) + : ""; + + return { + key: secretKey, + value: secretValue, + comment: secretComment, + version: encSecret.version + }; +}; diff --git a/backend/src/db/seeds/1-user.ts b/backend/src/db/seeds/1-user.ts index ca0042a98..8e2b00b90 100644 --- a/backend/src/db/seeds/1-user.ts +++ b/backend/src/db/seeds/1-user.ts @@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise { const [user] = await knex(TableName.Users) .insert([ { - // @ts-expect-error exluded type id needs to be inserted here to keep it testable + // eslint-disable-next-line + // @ts-ignore id: seedData1.id, email: seedData1.email, superAdmin: true, @@ -48,7 +49,8 @@ export async function seed(knex: Knex): Promise { ]); await knex(TableName.AuthTokenSession).insert({ - // @ts-expect-error exluded type id needs to be inserted here to keep it testable + // eslint-disable-next-line + // @ts-ignore id: seedData1.token.id, userId: seedData1.id, ip: "151.196.220.213", diff --git a/backend/src/db/seeds/2-org.ts b/backend/src/db/seeds/2-org.ts index a9c3ec3c7..ba2f65a36 100644 --- a/backend/src/db/seeds/2-org.ts +++ b/backend/src/db/seeds/2-org.ts @@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise { const [org] = await knex(TableName.Organization) .insert([ { - // @ts-expect-error exluded type id needs to be inserted here to keep it testable + // eslint-disable-next-line + // @ts-ignore id: seedData1.organization.id, name: "infisical", slug: "infisical", diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 7818d5831..fea71b557 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -1,7 +1,11 @@ +import crypto from "node:crypto"; + import { Knex } from "knex"; -import { OrgMembershipRole, TableName } from "../schemas"; -import { seedData1 } from "../seed-data"; +import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; + +import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; +import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ { name: "Development", slug: "dev" }, @@ -20,21 +24,32 @@ export async function seed(knex: Knex): Promise { name: seedData1.project.name, orgId: seedData1.organization.id, slug: "first-project", - // @ts-expect-error exluded type id needs to be inserted here to keep it testable + // eslint-disable-next-line + // @ts-ignore id: seedData1.project.id }) .returning("*"); - // await knex(TableName.ProjectKeys).insert({ - // projectId: project.id, - // senderId: seedData1.id - // }); - await knex(TableName.ProjectMembership).insert({ projectId: project.id, role: OrgMembershipRole.Admin, userId: seedData1.id }); + + const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first(); + if (!user) throw new Error("User not found"); + + const userPrivateKey = await getUserPrivateKey(seedData1.password, user); + const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey); + await knex(TableName.ProjectKeys).insert({ + projectId: project.id, + nonce: projectKey.nonce, + encryptedKey: projectKey.ciphertext, + receiverId: seedData1.id, + senderId: seedData1.id + }); + + // create default environments and default folders const envs = await knex(TableName.Environment) .insert( DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({ @@ -46,4 +61,19 @@ export async function seed(knex: Knex): Promise { ) .returning("*"); await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null }))); + + // save secret secret blind index + const encKey = process.env.ENCRYPTION_KEY; + if (!encKey) throw new Error("Missing ENCRYPTION_KEY"); + const salt = crypto.randomBytes(16).toString("base64"); + const secretBlindIndex = encryptSymmetric128BitHexKeyUTF8(salt, encKey); + // insert secret blind index for project + await knex(TableName.SecretBlindIndex).insert({ + projectId: project.id, + encryptedSaltCipherText: secretBlindIndex.ciphertext, + saltIV: secretBlindIndex.iv, + saltTag: secretBlindIndex.tag, + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.UTF8 + }); } diff --git a/backend/src/db/seeds/4-machine-identity.ts b/backend/src/db/seeds/4-machine-identity.ts new file mode 100644 index 000000000..149fba40a --- /dev/null +++ b/backend/src/db/seeds/4-machine-identity.ts @@ -0,0 +1,83 @@ +import bcrypt from "bcrypt"; +import { Knex } from "knex"; + +import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; +import { seedData1 } from "../seed-data"; + +export async function seed(knex: Knex): Promise { + // Deletes ALL existing entries + await knex(TableName.Identity).del(); + await knex(TableName.IdentityOrgMembership).del(); + + // Inserts seed entries + await knex(TableName.Identity).insert([ + { + // eslint-disable-next-line + // @ts-ignore + id: seedData1.machineIdentity.id, + name: seedData1.machineIdentity.name, + authMethod: IdentityAuthMethod.Univeral + } + ]); + const identityUa = await knex(TableName.IdentityUniversalAuth) + .insert([ + { + identityId: seedData1.machineIdentity.id, + clientId: seedData1.machineIdentity.clientCredentials.id, + clientSecretTrustedIps: JSON.stringify([ + { + type: "ipv4", + prefix: 0, + ipAddress: "0.0.0.0" + }, + { + type: "ipv6", + prefix: 0, + ipAddress: "::" + } + ]), + accessTokenTrustedIps: JSON.stringify([ + { + type: "ipv4", + prefix: 0, + ipAddress: "0.0.0.0" + }, + { + type: "ipv6", + prefix: 0, + ipAddress: "::" + } + ]), + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0 + } + ]) + .returning("*"); + const clientSecretHash = await bcrypt.hash(seedData1.machineIdentity.clientCredentials.secret, 10); + await knex(TableName.IdentityUaClientSecret).insert([ + { + identityUAId: identityUa[0].id, + description: "", + clientSecretTTL: 0, + clientSecretNumUses: 0, + clientSecretNumUsesLimit: 0, + clientSecretPrefix: seedData1.machineIdentity.clientCredentials.secret.slice(0, 4), + clientSecretHash, + isClientSecretRevoked: false + } + ]); + await knex(TableName.IdentityOrgMembership).insert([ + { + identityId: seedData1.machineIdentity.id, + orgId: seedData1.organization.id, + role: OrgMembershipRole.Admin + } + ]); + + await knex(TableName.IdentityProjectMembership).insert({ + identityId: seedData1.machineIdentity.id, + role: ProjectMembershipRole.Admin, + projectId: seedData1.project.id + }); +} diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 2ed439316..fb92aa3c5 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -3,6 +3,7 @@ import { registerOrgRoleRouter } from "./org-role-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRouter } from "./project-router"; import { registerSamlRouter } from "./saml-router"; +import { registerScimRouter } from "./scim-router"; import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router"; import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router"; import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; @@ -33,6 +34,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { prefix: "/secret-rotation-providers" }); await server.register(registerSamlRouter, { prefix: "/sso" }); + await server.register(registerScimRouter, { prefix: "/scim" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" }); diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts new file mode 100644 index 000000000..095815212 --- /dev/null +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -0,0 +1,331 @@ +import { z } from "zod"; + +import { ScimTokensSchema } from "@app/db/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerScimRouter = async (server: FastifyZodProvider) => { + server.addContentTypeParser("application/scim+json", { parseAs: "string" }, function (req, body, done) { + try { + const strBody = body instanceof Buffer ? body.toString() : body; + + const json: unknown = JSON.parse(strBody); + done(null, json); + } catch (err) { + const error = err as Error; + done(error, undefined); + } + }); + + server.route({ + url: "/scim-tokens", + method: "POST", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z.object({ + organizationId: z.string().trim(), + description: z.string().trim().default(""), + ttlDays: z.number().min(0).default(0) + }), + response: { + 200: z.object({ + scimToken: z.string().trim() + }) + } + }, + handler: async (req) => { + const { scimToken } = await server.services.scim.createScimToken({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + orgId: req.body.organizationId, + description: req.body.description, + ttlDays: req.body.ttlDays + }); + + return { scimToken }; + } + }); + + server.route({ + url: "/scim-tokens", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + querystring: z.object({ + organizationId: z.string().trim() + }), + response: { + 200: z.object({ + scimTokens: z.array(ScimTokensSchema) + }) + } + }, + handler: async (req) => { + const scimTokens = await server.services.scim.listScimTokens({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + orgId: req.query.organizationId + }); + + return { scimTokens }; + } + }); + + server.route({ + url: "/scim-tokens/:scimTokenId", + method: "DELETE", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + scimTokenId: z.string().trim() + }), + response: { + 200: z.object({ + scimToken: ScimTokensSchema + }) + } + }, + handler: async (req) => { + const scimToken = await server.services.scim.deleteScimToken({ + scimTokenId: req.params.scimTokenId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId + }); + + return { scimToken }; + } + }); + + // SCIM server endpoints + server.route({ + url: "/Users", + method: "GET", + schema: { + querystring: z.object({ + startIndex: z.coerce.number().default(1), + count: z.coerce.number().default(20), + filter: z.string().trim().optional() + }), + response: { + 200: z.object({ + Resources: z.array( + z.object({ + id: z.string().trim(), + userName: z.string().trim(), + name: z.object({ + familyName: z.string().trim(), + givenName: z.string().trim() + }), + emails: z.array( + z.object({ + primary: z.boolean(), + value: z.string().email(), + type: z.string().trim() + }) + ), + displayName: z.string().trim(), + active: z.boolean() + }) + ), + itemsPerPage: z.number(), + schemas: z.array(z.string()), + startIndex: z.number(), + totalResults: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), + handler: async (req) => { + const users = await req.server.services.scim.listScimUsers({ + offset: req.query.startIndex, + limit: req.query.count, + filter: req.query.filter, + orgId: req.permission.orgId as string + }); + return users; + } + }); + + server.route({ + url: "/Users/:userId", + method: "GET", + schema: { + params: z.object({ + userId: z.string().trim() + }), + response: { + 201: z.object({ + schemas: z.array(z.string()), + id: z.string().trim(), + userName: z.string().trim(), + name: z.object({ + familyName: z.string().trim(), + givenName: z.string().trim() + }), + emails: z.array( + z.object({ + primary: z.boolean(), + value: z.string().email(), + type: z.string().trim() + }) + ), + displayName: z.string().trim(), + active: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), + handler: async (req) => { + const user = await req.server.services.scim.getScimUser({ + userId: req.params.userId, + orgId: req.permission.orgId as string + }); + return user; + } + }); + + server.route({ + url: "/Users", + method: "POST", + schema: { + body: z.object({ + schemas: z.array(z.string()), + userName: z.string().trim().email(), + name: z.object({ + familyName: z.string().trim(), + givenName: z.string().trim() + }), + // emails: z.array( // optional? + // z.object({ + // primary: z.boolean(), + // value: z.string().email(), + // type: 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().email(), + 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 user = await req.server.services.scim.createScimUser({ + email: req.body.userName, + firstName: req.body.name.givenName, + lastName: req.body.name.familyName, + orgId: req.permission.orgId as string + }); + + return user; + } + }); + + server.route({ + url: "/Users/:userId", + method: "PATCH", + schema: { + params: z.object({ + userId: z.string().trim() + }), + body: z.object({ + schemas: z.array(z.string()), + Operations: z.array( + z.object({ + op: z.string().trim(), + path: z.string().trim().optional(), + value: z.union([ + z.object({ + active: z.boolean() + }), + z.string().trim() + ]) + }) + ) + }), + response: { + 200: z.object({}) + } + }, + onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), + handler: async (req) => { + const user = await req.server.services.scim.updateScimUser({ + userId: req.params.userId, + orgId: req.permission.orgId as string, + operations: req.body.Operations + }); + return user; + } + }); + + server.route({ + url: "/Users/:userId", + method: "PUT", + schema: { + params: z.object({ + userId: 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() + }) + } + }, + onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), + handler: async (req) => { + const user = await req.server.services.scim.replaceScimUser({ + userId: req.params.userId, + orgId: req.permission.orgId as string, + active: req.body.active + }); + return user; + } + }); +}; diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index 1c7868fc2..c4d4aabc0 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -58,6 +58,7 @@ export const auditLogServiceFactory = ({ if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) { if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" }); } + return auditLogQueue.pushToLog(data); }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 011dffebe..2aaaf7562 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -15,7 +15,7 @@ export type TListProjectAuditLogDTO = { export type TCreateAuditLogDTO = { event: Event; - actor: UserActor | IdentityActor | ServiceActor; + actor: UserActor | IdentityActor | ServiceActor | ScimClientActor; orgId?: string; projectId?: string; } & BaseAuthData; @@ -105,6 +105,8 @@ interface IdentityActorMetadata { name: string; } +interface ScimClientActorMetadata {} + export interface UserActor { type: ActorType.USER; metadata: UserActorMetadata; @@ -120,7 +122,12 @@ export interface IdentityActor { metadata: IdentityActorMetadata; } -export type Actor = UserActor | ServiceActor | IdentityActor; +export interface ScimClientActor { + type: ActorType.SCIM_CLIENT; + metadata: ScimClientActorMetadata; +} + +export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts new file mode 100644 index 000000000..e404aaace --- /dev/null +++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts @@ -0,0 +1,27 @@ +export const getDefaultOnPremFeatures = () => { + return { + _id: null, + slug: null, + tier: -1, + workspaceLimit: null, + workspacesUsed: 0, + memberLimit: null, + membersUsed: 0, + environmentLimit: null, + environmentsUsed: 0, + secretVersioning: true, + pitRecovery: false, + ipAllowlisting: true, + rbac: false, + customRateLimits: false, + customAlerts: false, + auditLogs: false, + auditLogsRetentionDays: 0, + samlSSO: false, + status: null, + trial_end: null, + has_used_trial: true, + secretApproval: false, + secretRotation: true + }; +}; diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 7014eac5f..9b1d4c203 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ auditLogs: false, auditLogsRetentionDays: 0, samlSSO: false, + scim: false, status: null, trial_end: null, has_used_trial: true, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 762aff5b2..354714489 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -25,6 +25,7 @@ export type TFeatureSet = { auditLogs: false; auditLogsRetentionDays: 0; samlSSO: false; + scim: false; status: null; trial_end: null; has_used_trial: true; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index cc18af8ae..e527f1a4a 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -16,6 +16,7 @@ export enum OrgPermissionSubjects { Settings = "settings", IncidentAccount = "incident-contact", Sso = "sso", + Scim = "scim", Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity" @@ -29,6 +30,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.Sso] + | [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity]; @@ -69,6 +71,11 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Scim); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 767729179..b541e16be 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -195,7 +195,7 @@ export const samlConfigServiceFactory = ({ updateQuery.certTag = certTag; } const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); - await orgDAL.updateById(orgId, { authEnforced: false }); + await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false }); return ssoConfig; }; @@ -338,7 +338,8 @@ export const samlConfigServiceFactory = ({ email, firstName, lastName, - authMethods: [AuthMethod.EMAIL] + authMethods: [AuthMethod.EMAIL], + isGhost: false }, tx ); diff --git a/backend/src/ee/services/scim/scim-dal.ts b/backend/src/ee/services/scim/scim-dal.ts new file mode 100644 index 000000000..05c21b80c --- /dev/null +++ b/backend/src/ee/services/scim/scim-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TScimDALFactory = ReturnType; + +export const scimDALFactory = (db: TDbClient) => { + const scimTokenOrm = ormify(db, TableName.ScimToken); + return scimTokenOrm; +}; diff --git a/backend/src/ee/services/scim/scim-fns.ts b/backend/src/ee/services/scim/scim-fns.ts new file mode 100644 index 000000000..b5f35c9ee --- /dev/null +++ b/backend/src/ee/services/scim/scim-fns.ts @@ -0,0 +1,58 @@ +import { TListScimUsers, TScimUser } from "./scim-types"; + +export const buildScimUserList = ({ + scimUsers, + offset, + limit +}: { + scimUsers: TScimUser[]; + offset: number; + limit: number; +}): TListScimUsers => { + return { + Resources: scimUsers, + itemsPerPage: limit, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: offset, + totalResults: scimUsers.length + }; +}; + +export const buildScimUser = ({ + userId, + firstName, + lastName, + email, + active +}: { + userId: string; + firstName: string; + lastName: string; + email: string; + active: boolean; +}): TScimUser => { + return { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], + id: userId, + userName: email, + displayName: `${firstName} ${lastName}`, + name: { + givenName: firstName, + middleName: null, + familyName: lastName + }, + emails: [ + { + primary: true, + value: email, + type: "work" + } + ], + active, + groups: [], + meta: { + resourceType: "User", + location: null + } + }; +}; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts new file mode 100644 index 000000000..33b2f2c84 --- /dev/null +++ b/backend/src/ee/services/scim/scim-service.ts @@ -0,0 +1,431 @@ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; +import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; +import { TOrgPermission } from "@app/lib/types"; +import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { deleteOrgMembership } from "@app/services/org/org-fns"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { buildScimUser, buildScimUserList } from "./scim-fns"; +import { + TCreateScimTokenDTO, + TCreateScimUserDTO, + TDeleteScimTokenDTO, + TGetScimUserDTO, + TListScimUsers, + TListScimUsersDTO, + TReplaceScimUserDTO, + TScimTokenJwtPayload, + TUpdateScimUserDTO +} from "./scim-types"; + +type TScimServiceFactoryDep = { + scimDAL: Pick; + userDAL: Pick; + orgDAL: Pick< + TOrgDALFactory, + "createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" + >; + projectDAL: Pick; + projectMembershipDAL: Pick; + licenseService: Pick; + permissionService: Pick; + smtpService: TSmtpService; +}; + +export type TScimServiceFactory = ReturnType; + +export const scimServiceFactory = ({ + licenseService, + scimDAL, + userDAL, + orgDAL, + projectDAL, + projectMembershipDAL, + permissionService, + smtpService +}: TScimServiceFactoryDep) => { + const createScimToken = async ({ actor, actorId, actorOrgId, orgId, description, ttlDays }: TCreateScimTokenDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim); + + const plan = await licenseService.getPlan(orgId); + if (!plan.scim) + throw new BadRequestError({ + message: "Failed to create a SCIM token due to plan restriction. Upgrade plan to create a SCIM token." + }); + + const appCfg = getConfig(); + + const scimTokenData = await scimDAL.create({ + orgId, + description, + ttlDays + }); + + const scimToken = jwt.sign( + { + scimTokenId: scimTokenData.id, + authTokenType: AuthTokenType.SCIM_TOKEN + }, + appCfg.AUTH_SECRET + ); + + return { scimToken }; + }; + + const listScimTokens = async ({ actor, actorId, actorOrgId, orgId }: TOrgPermission) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); + + const plan = await licenseService.getPlan(orgId); + if (!plan.scim) + throw new BadRequestError({ + message: "Failed to get SCIM tokens due to plan restriction. Upgrade plan to get SCIM tokens." + }); + + const scimTokens = await scimDAL.find({ orgId }); + return scimTokens; + }; + + const deleteScimToken = async ({ scimTokenId, actor, actorId, actorOrgId }: TDeleteScimTokenDTO) => { + let scimToken = await scimDAL.findById(scimTokenId); + if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" }); + + const { permission } = await permissionService.getOrgPermission(actor, actorId, scimToken.orgId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); + + const plan = await licenseService.getPlan(scimToken.orgId); + if (!plan.scim) + throw new BadRequestError({ + message: "Failed to delete the SCIM token due to plan restriction. Upgrade plan to delete the SCIM token." + }); + + scimToken = await scimDAL.deleteById(scimTokenId); + + return scimToken; + }; + + // SCIM server endpoints + const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise => { + const org = await orgDAL.findById(orgId); + + if (!org.scimEnabled) + throw new ScimRequestError({ + detail: "SCIM is disabled for the organization", + status: 403 + }); + + const parseFilter = (filterToParse: string | undefined) => { + if (!filterToParse) return {}; + const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim()); + + let attributeName = parsedName; + if (parsedName === "userName") { + attributeName = "email"; + } + + return { [attributeName]: parsedValue }; + }; + + const findOpts = { + ...(offset && { offset }), + ...(limit && { limit }) + }; + + const users = await orgDAL.findMembership( + { + orgId, + ...parseFilter(filter) + }, + findOpts + ); + + const scimUsers = users.map(({ userId, firstName, lastName, email }) => + buildScimUser({ + userId: userId ?? "", + firstName: firstName ?? "", + lastName: lastName ?? "", + email, + active: true + }) + ); + + return buildScimUserList({ + scimUsers, + offset, + limit + }); + }; + + const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => { + const [membership] = await orgDAL + .findMembership({ + userId, + orgId + }) + .catch(() => { + throw new ScimRequestError({ + detail: "User not found", + status: 404 + }); + }); + + if (!membership) + throw new ScimRequestError({ + detail: "User not found", + status: 404 + }); + + if (!membership.scimEnabled) + throw new ScimRequestError({ + detail: "SCIM is disabled for the organization", + status: 403 + }); + + return buildScimUser({ + userId: membership.userId as string, + firstName: membership.firstName as string, + lastName: membership.lastName as string, + email: membership.email, + active: true + }); + }; + + const createScimUser = async ({ firstName, lastName, email, orgId }: TCreateScimUserDTO) => { + 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 + }); + + let user = await userDAL.findOne({ + email + }); + + if (user) { + await userDAL.transaction(async (tx) => { + const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx }); + if (orgMembership) + throw new ScimRequestError({ + detail: "User already exists in the database", + status: 409 + }); + + if (!orgMembership) { + await orgDAL.createMembership( + { + userId: user.id, + orgId, + inviteEmail: email, + role: OrgMembershipRole.Member, + status: OrgMembershipStatus.Invited + }, + tx + ); + } + }); + } else { + user = await userDAL.transaction(async (tx) => { + const newUser = await userDAL.create( + { + email, + firstName, + lastName, + authMethods: [AuthMethod.EMAIL], + isGhost: false + }, + tx + ); + + await orgDAL.createMembership( + { + inviteEmail: email, + orgId, + userId: newUser.id, + role: OrgMembershipRole.Member, + status: OrgMembershipStatus.Invited + }, + tx + ); + return newUser; + }); + } + + const appCfg = getConfig(); + await smtpService.sendMail({ + template: SmtpTemplates.ScimUserProvisioned, + subjectLine: "Infisical organization invitation", + recipients: [email], + substitutions: { + organizationName: org.name, + callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}` + } + }); + + return buildScimUser({ + userId: user.id, + firstName: user.firstName as string, + lastName: user.lastName as string, + email: user.email, + active: true + }); + }; + + const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => { + const [membership] = await orgDAL + .findMembership({ + userId, + orgId + }) + .catch(() => { + throw new ScimRequestError({ + detail: "User not found", + status: 404 + }); + }); + + if (!membership) + throw new ScimRequestError({ + detail: "User not found", + status: 404 + }); + + if (!membership.scimEnabled) + throw new ScimRequestError({ + detail: "SCIM is disabled for the organization", + status: 403 + }); + + let active = true; + + operations.forEach((operation) => { + if (operation.op.toLowerCase() === "replace") { + if (operation.path === "active" && operation.value === "False") { + // azure scim op format + active = false; + } else if (typeof operation.value === "object" && operation.value.active === false) { + // okta scim op format + active = false; + } + } + }); + + if (!active) { + await deleteOrgMembership({ + orgMembershipId: membership.id, + orgId: membership.orgId, + orgDAL, + projectDAL, + projectMembershipDAL + }); + } + + return buildScimUser({ + userId: membership.userId as string, + firstName: membership.firstName as string, + lastName: membership.lastName as string, + email: membership.email, + active + }); + }; + + const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => { + const [membership] = await orgDAL + .findMembership({ + userId, + orgId + }) + .catch(() => { + throw new ScimRequestError({ + detail: "User not found", + status: 404 + }); + }); + + if (!membership) + throw new ScimRequestError({ + detail: "User not found", + status: 404 + }); + + if (!membership.scimEnabled) + throw new ScimRequestError({ + detail: "SCIM is disabled for the organization", + status: 403 + }); + + if (!active) { + // tx + await deleteOrgMembership({ + orgMembershipId: membership.id, + orgId: membership.orgId, + orgDAL, + projectDAL, + projectMembershipDAL + }); + } + + return buildScimUser({ + userId: membership.userId as string, + firstName: membership.firstName as string, + lastName: membership.lastName as string, + email: membership.email, + active + }); + }; + + const fnValidateScimToken = async (token: TScimTokenJwtPayload) => { + const scimToken = await scimDAL.findById(token.scimTokenId); + if (!scimToken) throw new UnauthorizedError(); + + const { ttlDays, createdAt } = scimToken; + + // ttl check + if (Number(ttlDays) > 0) { + const currentDate = new Date(); + const scimTokenCreatedAt = new Date(createdAt); + const ttlInMilliseconds = Number(scimToken.ttlDays) * 86400 * 1000; + const expirationDate = new Date(scimTokenCreatedAt.getTime() + ttlInMilliseconds); + + if (currentDate > expirationDate) + throw new ScimRequestError({ + detail: "The access token expired", + status: 401 + }); + } + + return { scimTokenId: scimToken.id, orgId: scimToken.orgId }; + }; + + return { + createScimToken, + listScimTokens, + deleteScimToken, + listScimUsers, + getScimUser, + createScimUser, + updateScimUser, + replaceScimUser, + fnValidateScimToken + }; +}; diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts new file mode 100644 index 000000000..9751d591f --- /dev/null +++ b/backend/src/ee/services/scim/scim-types.ts @@ -0,0 +1,87 @@ +import { TOrgPermission } from "@app/lib/types"; + +export type TCreateScimTokenDTO = { + description: string; + ttlDays: number; +} & TOrgPermission; + +export type TDeleteScimTokenDTO = { + scimTokenId: string; +} & Omit; + +// SCIM server endpoint types + +export type TListScimUsersDTO = { + offset: number; + limit: number; + filter?: string; + orgId: string; +}; + +export type TListScimUsers = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"]; + totalResults: number; + Resources: TScimUser[]; + itemsPerPage: number; + startIndex: number; +}; + +export type TGetScimUserDTO = { + userId: string; + orgId: string; +}; + +export type TCreateScimUserDTO = { + email: string; + firstName: string; + lastName: string; + orgId: string; +}; + +export type TUpdateScimUserDTO = { + userId: string; + orgId: string; + operations: { + op: string; + path?: string; + value?: + | string + | { + active: boolean; + }; + }[]; +}; + +export type TReplaceScimUserDTO = { + userId: string; + active: boolean; + orgId: string; +}; + +export type TScimTokenJwtPayload = { + scimTokenId: string; + authTokenType: string; +}; + +export type TScimUser = { + schemas: string[]; + id: string; + userName: string; + displayName: string; + name: { + givenName: string; + middleName: null; + familyName: string; + }; + emails: { + primary: boolean; + value: string; + type: string; + }[]; + active: boolean; + groups: string[]; + meta: { + resourceType: string; + location: null; + }; +}; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts index 9b4742255..736cd253e 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts @@ -1,8 +1,13 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; +import { + SecretApprovalRequestsSecretsSchema, + TableName, + TSecretApprovalRequestsSecrets, + TSecretTags +} from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TSecretApprovalRequestSecretDALFactory = ReturnType; @@ -11,6 +16,35 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); + const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { + try { + const existingApprovalSecrets = await secretApprovalRequestSecretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingApprovalSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); + } + + if (data.length === 0) return []; + + const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedApprovalSecrets; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + const findByRequestId = async (requestId: string, tx?: Knex) => { try { const doc = await (tx || db)({ @@ -190,6 +224,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { return { ...secretApprovalRequestSecretOrm, findByRequestId, + bulkUpdateNoVersionIncrement, insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany }; }; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index ef10db804..9a91b745d 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -11,6 +11,7 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { groupBy, pick, unique } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { ActorType } from "@app/services/auth/auth-type"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretQueueFactory } from "@app/services/secret/secret-queue"; import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; @@ -47,6 +48,7 @@ type TSecretApprovalRequestServiceFactoryDep = { secretBlindIndexDAL: Pick; snapshotService: Pick; secretVersionDAL: Pick; + projectDAL: Pick; secretService: Pick< TSecretServiceFactory, | "fnSecretBulkInsert" @@ -67,6 +69,7 @@ export const secretApprovalRequestServiceFactory = ({ secretApprovalRequestReviewerDAL, secretApprovalRequestSecretDAL, secretBlindIndexDAL, + projectDAL, permissionService, snapshotService, secretService, @@ -434,6 +437,8 @@ export const secretApprovalRequestServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" }); const folderId = folder.id; diff --git a/backend/src/lib/crypto/encryption.ts b/backend/src/lib/crypto/encryption.ts index 74febccec..16a7f42e7 100644 --- a/backend/src/lib/crypto/encryption.ts +++ b/backend/src/lib/crypto/encryption.ts @@ -8,6 +8,9 @@ import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "../config/env"; +export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s); +export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u); + export type TDecryptSymmetricInput = { ciphertext: string; iv: string; @@ -44,7 +47,7 @@ export const encryptSymmetric = (plaintext: string, key: string) => { }; }; -export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => { +export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string | Buffer) => { const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16); const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv); @@ -58,7 +61,12 @@ export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) }; }; -export const decryptSymmetric128BitHexKeyUTF8 = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => { +export const decryptSymmetric128BitHexKeyUTF8 = ({ + ciphertext, + iv, + tag, + key +}: Omit & { key: string | Buffer }): string => { const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64")); decipher.setAuthTag(Buffer.from(tag, "base64")); diff --git a/backend/src/lib/crypto/index.ts b/backend/src/lib/crypto/index.ts index 62278de1d..9d7f4e886 100644 --- a/backend/src/lib/crypto/index.ts +++ b/backend/src/lib/crypto/index.ts @@ -1,12 +1,20 @@ export { buildSecretBlindIndexFromName, createSecretBlindIndex, + decodeBase64, decryptAsymmetric, decryptSymmetric, decryptSymmetric128BitHexKeyUTF8, + encodeBase64, encryptAsymmetric, encryptSymmetric, encryptSymmetric128BitHexKeyUTF8, generateAsymmetricKeyPair } from "./encryption"; +export { + decryptIntegrationAuths, + decryptSecretApprovals, + decryptSecrets, + decryptSecretVersions +} from "./secret-encryption"; export { generateSrpServerKey, srpCheckClientProof } from "./srp"; diff --git a/backend/src/lib/crypto/secret-encryption.ts b/backend/src/lib/crypto/secret-encryption.ts new file mode 100644 index 000000000..2e0492560 --- /dev/null +++ b/backend/src/lib/crypto/secret-encryption.ts @@ -0,0 +1,293 @@ +import crypto from "crypto"; +import { z } from "zod"; + +import { + IntegrationAuthsSchema, + SecretApprovalRequestsSecretsSchema, + SecretsSchema, + SecretVersionsSchema, + TIntegrationAuths, + TProjectKeys, + TSecretApprovalRequestsSecrets, + TSecrets, + TSecretVersions +} from "../../db/schemas"; +import { decryptAsymmetric } from "./encryption"; + +const DecryptedValuesSchema = z.object({ + id: z.string(), + secretKey: z.string(), + secretValue: z.string(), + secretComment: z.string().optional() +}); + +const DecryptedSecretSchema = z.object({ + decrypted: DecryptedValuesSchema, + original: SecretsSchema +}); + +const DecryptedIntegrationAuthsSchema = z.object({ + decrypted: z.object({ + id: z.string(), + access: z.string(), + accessId: z.string(), + refresh: z.string() + }), + original: IntegrationAuthsSchema +}); + +const DecryptedSecretVersionsSchema = z.object({ + decrypted: DecryptedValuesSchema, + original: SecretVersionsSchema +}); + +const DecryptedSecretApprovalsSchema = z.object({ + decrypted: DecryptedValuesSchema, + original: SecretApprovalRequestsSecretsSchema +}); + +type DecryptedSecret = z.infer; +type DecryptedSecretVersions = z.infer; +type DecryptedSecretApprovals = z.infer; +type DecryptedIntegrationAuths = z.infer; + +type TLatestKey = TProjectKeys & { + sender: { + publicKey: string; + }; +}; + +const decryptCipher = ({ + ciphertext, + iv, + tag, + key +}: { + ciphertext: string; + iv: string; + tag: string; + key: string | Buffer; +}) => { + const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64")); + decipher.setAuthTag(Buffer.from(tag, "base64")); + + let cleartext = decipher.update(ciphertext, "base64", "utf8"); + cleartext += decipher.final("utf8"); + + return cleartext; +}; + +const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: string }>, key: string | Buffer) => { + const results: string[] = []; + + for (const { ciphertext, iv, tag } of data) { + if (!ciphertext || !iv || !tag) { + results.push(""); + } else { + results.push(decryptCipher({ ciphertext, iv, tag, key })); + } + } + + return results; +}; +export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => { + const key = decryptAsymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey + }); + + const decryptedSecrets: DecryptedSecret[] = []; + + encryptedSecrets.forEach((encSecret) => { + const [secretKey, secretValue, secretComment] = getDecryptedValues( + [ + { + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag + }, + { + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag + }, + { + ciphertext: encSecret.secretCommentCiphertext || "", + iv: encSecret.secretCommentIV || "", + tag: encSecret.secretCommentTag || "" + } + ], + key + ); + + const decryptedSecret: DecryptedSecret = { + decrypted: { + secretKey, + secretValue, + secretComment, + id: encSecret.id + }, + original: encSecret + }; + + decryptedSecrets.push(DecryptedSecretSchema.parse(decryptedSecret)); + }); + + return decryptedSecrets; +}; + +export const decryptSecretVersions = ( + encryptedSecretVersions: TSecretVersions[], + privateKey: string, + latestKey: TLatestKey +) => { + const key = decryptAsymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey + }); + + const decryptedSecrets: DecryptedSecretVersions[] = []; + + encryptedSecretVersions.forEach((encSecret) => { + const [secretKey, secretValue, secretComment] = getDecryptedValues( + [ + { + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag + }, + { + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag + }, + { + ciphertext: encSecret.secretCommentCiphertext || "", + iv: encSecret.secretCommentIV || "", + tag: encSecret.secretCommentTag || "" + } + ], + key + ); + + const decryptedSecret: DecryptedSecretVersions = { + decrypted: { + secretKey, + secretValue, + secretComment, + id: encSecret.id + }, + original: encSecret + }; + + decryptedSecrets.push(DecryptedSecretVersionsSchema.parse(decryptedSecret)); + }); + + return decryptedSecrets; +}; + +export const decryptSecretApprovals = ( + encryptedSecretApprovals: TSecretApprovalRequestsSecrets[], + privateKey: string, + latestKey: TLatestKey +) => { + const key = decryptAsymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey + }); + + const decryptedSecrets: DecryptedSecretApprovals[] = []; + + encryptedSecretApprovals.forEach((encApproval) => { + const [secretKey, secretValue, secretComment] = getDecryptedValues( + [ + { + ciphertext: encApproval.secretKeyCiphertext, + iv: encApproval.secretKeyIV, + tag: encApproval.secretKeyTag + }, + { + ciphertext: encApproval.secretValueCiphertext, + iv: encApproval.secretValueIV, + tag: encApproval.secretValueTag + }, + { + ciphertext: encApproval.secretCommentCiphertext || "", + iv: encApproval.secretCommentIV || "", + tag: encApproval.secretCommentTag || "" + } + ], + key + ); + + const decryptedSecret: DecryptedSecretApprovals = { + decrypted: { + secretKey, + secretValue, + secretComment, + id: encApproval.id + }, + original: encApproval + }; + + decryptedSecrets.push(DecryptedSecretApprovalsSchema.parse(decryptedSecret)); + }); + + return decryptedSecrets; +}; + +export const decryptIntegrationAuths = ( + encryptedIntegrationAuths: TIntegrationAuths[], + privateKey: string, + latestKey: TLatestKey +) => { + const key = decryptAsymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey + }); + + const decryptedIntegrationAuths: DecryptedIntegrationAuths[] = []; + + encryptedIntegrationAuths.forEach((encAuth) => { + const [access, accessId, refresh] = getDecryptedValues( + [ + { + ciphertext: encAuth.accessCiphertext || "", + iv: encAuth.accessIV || "", + tag: encAuth.accessTag || "" + }, + { + ciphertext: encAuth.accessIdCiphertext || "", + iv: encAuth.accessIdIV || "", + tag: encAuth.accessIdTag || "" + }, + { + ciphertext: encAuth.refreshCiphertext || "", + iv: encAuth.refreshIV || "", + tag: encAuth.refreshTag || "" + } + ], + key + ); + + decryptedIntegrationAuths.push({ + decrypted: { + id: encAuth.id, + access, + accessId, + refresh + }, + original: encAuth + }); + }); + + return decryptedIntegrationAuths; +}; diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index b05f3734d..bc29cdb3f 100644 --- a/backend/src/lib/crypto/srp.ts +++ b/backend/src/lib/crypto/srp.ts @@ -1,4 +1,12 @@ +import argon2 from "argon2"; +import crypto from "crypto"; import jsrp from "jsrp"; +import nacl from "tweetnacl"; +import tweetnacl from "tweetnacl-util"; + +import { TUserEncryptionKeys } from "@app/db/schemas"; + +import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption"; export const generateSrpServerKey = async (salt: string, verifier: string) => { // eslint-disable-next-line new-cap @@ -24,3 +32,99 @@ export const srpCheckClientProof = async ( server.setClientPublicKey(clientPublicKey); return server.checkClientProof(clientProof); }; + +// Ghost user related: +// 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. +export const generateUserSrpKeys = async (email: string, password: string) => { + const pair = nacl.box.keyPair(); + const secretKeyUint8Array = pair.secretKey; + const publicKeyUint8Array = pair.publicKey; + const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array); + const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array); + + // eslint-disable-next-line + const client = new jsrp.client(); + await new Promise((resolve) => { + client.init({ username: email, password }, () => resolve(null)); + }); + const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>((resolve, reject) => { + client.createVerifier((err, res) => { + if (err) return reject(err); + return resolve(res); + }); + }); + const derivedKey = await argon2.hash(password, { + salt: Buffer.from(salt), + memoryCost: 65536, + timeCost: 3, + parallelism: 1, + hashLength: 32, + type: argon2.argon2id, + raw: true + }); + if (!derivedKey) throw new Error("Failed to derive key from password"); + + const key = crypto.randomBytes(32); + + // create encrypted private key by encrypting the private + // key with the symmetric key [key] + const { + ciphertext: encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + } = encryptSymmetric(privateKey, key.toString("base64")); + + // create the protected key by encrypting the symmetric key + // [key] with the derived key + const { + ciphertext: protectedKey, + iv: protectedKeyIV, + tag: protectedKeyTag + } = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64")); + + return { + protectedKey, + plainPrivateKey: privateKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + }; +}; + +export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { + const derivedKey = await argon2.hash(password, { + salt: Buffer.from(user.salt), + memoryCost: 65536, + timeCost: 3, + parallelism: 1, + hashLength: 32, + type: argon2.argon2id, + raw: true + }); + if (!derivedKey) throw new Error("Failed to derive key from password"); + const key = decryptSymmetric({ + ciphertext: user.protectedKey!, + iv: user.protectedKeyIV!, + tag: user.protectedKeyTag!, + key: derivedKey.toString("base64") + }); + const privateKey = decryptSymmetric({ + ciphertext: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag, + key + }); + return privateKey; +}; + +export const buildUserProjectKey = async (privateKey: string, publickey: string) => { + const randomBytes = crypto.randomBytes(16).toString("hex"); + const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey); + return { nonce, ciphertext }; +}; diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index b4376f007..d93244bbd 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -58,3 +58,35 @@ export class BadRequestError extends Error { this.error = error; } } + +export class ScimRequestError extends Error { + name: string; + + schemas: string[]; + + detail: string; + + status: number; + + error: unknown; + + constructor({ + name, + error, + detail, + status + }: { + message?: string; + name?: string; + error?: unknown; + detail: string; + status: number; + }) { + super(detail ?? "The request is invalid"); + this.name = name || "ScimRequestError"; + this.schemas = ["urn:ietf:params:scim:api:messages:2.0:Error"]; + this.error = error; + this.detail = detail; + this.status = status; + } +} diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 58c829549..e11e0b590 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -1,6 +1,7 @@ import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq"; import Redis from "ioredis"; +import { SecretKeyEncoding } from "@app/db/schemas"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TScanFullRepoEventPayload, @@ -15,7 +16,8 @@ export enum QueueName { IntegrationSync = "sync-integrations", SecretWebhook = "secret-webhook", SecretFullRepoScan = "secret-full-repo-scan", - SecretPushEventScan = "secret-push-event-scan" + SecretPushEventScan = "secret-push-event-scan", + UpgradeProjectToGhost = "upgrade-project-to-ghost" } export enum QueueJobs { @@ -25,7 +27,8 @@ export enum QueueJobs { AuditLogPrune = "audit-log-prune-job", SecWebhook = "secret-webhook-trigger", IntegrationSync = "secret-integration-pull", - SecretScan = "secret-scan" + SecretScan = "secret-scan", + UpgradeProjectToGhost = "upgrade-project-to-ghost-job" } export type TQueueJobTypes = { @@ -64,6 +67,20 @@ export type TQueueJobTypes = { payload: TScanFullRepoEventPayload; }; [QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload }; + + [QueueName.UpgradeProjectToGhost]: { + name: QueueJobs.UpgradeProjectToGhost; + payload: { + projectId: string; + startedByUserId: string; + encryptedPrivateKey: { + encryptedKey: string; + encryptedKeyIv: string; + encryptedKeyTag: string; + keyEncoding: SecretKeyEncoding; + }; + }; + }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index ca6b9003a..969d2fe43 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -37,7 +37,7 @@ type TMain = { export const main = async ({ db, smtp, logger, queue }: TMain) => { const appCfg = getConfig(); const server = fasitfy({ - logger, + logger: appCfg.NODE_ENV === "test" ? false : logger, trustProxy: true, connectionTimeout: 30 * 1000, ignoreTrailingSlash: true diff --git a/backend/src/server/plugins/audit-log.ts b/backend/src/server/plugins/audit-log.ts index b42cc2ff2..de352a9bc 100644 --- a/backend/src/server/plugins/audit-log.ts +++ b/backend/src/server/plugins/audit-log.ts @@ -63,6 +63,11 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => { identityId: req.auth.identityId } }; + } else if (req.auth.actor === ActorType.SCIM_CLIENT) { + payload.actor = { + type: ActorType.SCIM_CLIENT, + metadata: {} + }; } else { throw new BadRequestError({ message: "Missing logic for other actor" }); } diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 7abcd073c..cf8d9dea3 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -3,6 +3,7 @@ import fp from "fastify-plugin"; import jwt, { JwtPayload } from "jsonwebtoken"; import { TServiceTokens, TUsers } from "@app/db/schemas"; +import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { UnauthorizedError } from "@app/lib/errors"; import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; @@ -35,6 +36,12 @@ export type TAuthMode = actor: ActorType.IDENTITY; identityId: string; identityName: string; + } + | { + authMode: AuthMode.SCIM_TOKEN; + actor: ActorType.SCIM_CLIENT; + scimTokenId: string; + orgId: string; }; const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { @@ -55,6 +62,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { } const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload; + switch (decodedToken.authTokenType) { case AuthTokenType.ACCESS_TOKEN: return { @@ -70,6 +78,12 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { token: decodedToken as TIdentityAccessTokenJwtPayload, actor: ActorType.IDENTITY } as const; + case AuthTokenType.SCIM_TOKEN: + return { + authMode: AuthMode.SCIM_TOKEN, + token: decodedToken as TScimTokenJwtPayload, + actor: ActorType.SCIM_CLIENT + } as const; default: return { authMode: null, token: null } as const; } @@ -113,6 +127,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user }; break; } + case AuthMode.SCIM_TOKEN: { + const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); + req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId }; + break; + } default: throw new UnauthorizedError({ name: "Unknown token strategy" }); } diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index 572814d64..2d61647e8 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -14,6 +14,8 @@ export const injectPermission = fp(async (server) => { req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId }; } else if (req.auth.actor === ActorType.SERVICE) { req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId }; + } else if (req.auth.actor === ActorType.SCIM_CLIENT) { + req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId }; } }); }); diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 8587c93bd..c8da4077a 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -2,7 +2,13 @@ import { ForbiddenError } from "@casl/ability"; import fastifyPlugin from "fastify-plugin"; import { ZodError } from "zod"; -import { BadRequestError, DatabaseError, InternalServerError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + DatabaseError, + InternalServerError, + ScimRequestError, + UnauthorizedError +} from "@app/lib/errors"; export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => { server.setErrorHandler((error, req, res) => { @@ -21,6 +27,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider error: "PermissionDenied", message: `You are not allowed to ${error.action} on ${error.subjectType}` }); + } else if (error instanceof ScimRequestError) { + void res.status(error.status).send({ + schemas: error.schemas, + status: error.status, + detail: error.detail + }); } else { void res.send(error); } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 28f65ea99..9444aff16 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -11,6 +11,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; +import { scimDALFactory } from "@app/ee/services/scim/scim-dal"; +import { scimServiceFactory } from "@app/ee/services/scim/scim-service"; import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal"; import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal"; import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -63,6 +65,7 @@ import { orgRoleDALFactory } from "@app/services/org/org-role-dal"; import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgServiceFactory } from "@app/services/org/org-service"; import { projectDALFactory } from "@app/services/project/project-dal"; +import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectServiceFactory } from "@app/services/project/project-service"; import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; @@ -155,6 +158,7 @@ export const registerRoutes = async ( const auditLogDAL = auditLogDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db); + const scimDAL = scimDALFactory(db); // ee db layer ops const permissionDAL = permissionDALFactory(db); @@ -188,6 +192,7 @@ export const registerRoutes = async ( trustedIpDAL, permissionService }); + const auditLogQueue = auditLogQueueServiceFactory({ auditLogDAL, queueService, @@ -210,6 +215,16 @@ export const registerRoutes = async ( samlConfigDAL, licenseService }); + const scimService = scimServiceFactory({ + licenseService, + scimDAL, + userDAL, + orgDAL, + projectDAL, + projectMembershipDAL, + permissionService, + smtpService + }); const telemetryService = telemetryServiceFactory(); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); @@ -266,19 +281,13 @@ export const registerRoutes = async ( secretScanningDAL, secretScanningQueue }); - const projectService = projectServiceFactory({ - permissionService, - projectDAL, - secretBlindIndexDAL, - projectEnvDAL, - projectMembershipDAL, - folderDAL, - licenseService - }); + const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL }); + const projectMembershipService = projectMembershipServiceFactory({ projectMembershipDAL, projectDAL, permissionService, + projectBotDAL, orgDAL, userDAL, smtpService, @@ -286,6 +295,46 @@ export const registerRoutes = async ( projectRoleDAL, licenseService }); + const projectKeyService = projectKeyServiceFactory({ + permissionService, + projectKeyDAL, + projectMembershipDAL + }); + + const projectQueueService = projectQueueFactory({ + queueService, + secretDAL, + folderDAL, + projectDAL, + orgDAL, + integrationAuthDAL, + orgService, + projectEnvDAL, + userDAL, + secretVersionDAL, + projectKeyDAL, + projectBotDAL, + projectMembershipDAL, + secretApprovalRequestDAL, + secretApprovalSecretDAL: sarSecretDAL + }); + + const projectService = projectServiceFactory({ + permissionService, + projectDAL, + projectQueue: projectQueueService, + secretBlindIndexDAL, + identityProjectDAL, + identityOrgMembershipDAL, + projectBotDAL, + projectKeyDAL, + userDAL, + projectEnvDAL, + orgService, + projectMembershipDAL, + folderDAL, + licenseService + }); const projectEnvService = projectEnvServiceFactory({ permissionService, projectEnvDAL, @@ -293,11 +342,7 @@ export const registerRoutes = async ( projectDAL, folderDAL }); - const projectKeyService = projectKeyServiceFactory({ - permissionService, - projectKeyDAL, - projectMembershipDAL - }); + const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL }); const snapshotService = secretSnapshotServiceFactory({ @@ -332,9 +377,9 @@ export const registerRoutes = async ( folderDAL, permissionService, secretImportDAL, + projectDAL, secretDAL }); - const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL }); const integrationAuthService = integrationAuthServiceFactory({ integrationAuthDAL, integrationDAL, @@ -368,6 +413,7 @@ export const registerRoutes = async ( secretVersionTagDAL, secretBlindIndexDAL, permissionService, + projectDAL, secretDAL, secretTagDAL, snapshotService, @@ -381,6 +427,7 @@ export const registerRoutes = async ( secretTagDAL, secretApprovalRequestSecretDAL: sarSecretDAL, secretApprovalRequestReviewerDAL: sarReviewerDAL, + projectDAL, secretVersionDAL, secretBlindIndexDAL, secretApprovalRequestDAL, @@ -486,6 +533,7 @@ export const registerRoutes = async ( secretScanning: secretScanningService, license: licenseService, trustedIp: trustedIpService, + scim: scimService, secretBlindIndex: secretBlindIndexService, telemetry: telemetryService }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 2f26893d8..b89e3dc4c 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { SuperAdminSchema, UsersSchema } from "@app/db/schemas"; +import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { UnauthorizedError } from "@app/lib/errors"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; @@ -73,6 +73,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { 200: z.object({ message: z.string(), user: UsersSchema, + organization: OrganizationsSchema, token: z.string(), new: z.string() }) @@ -83,7 +84,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { const serverCfg = await getServerCfg(); if (serverCfg.initialized) throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" }); - const { user, token } = await server.services.superAdmin.adminSignUp({ + const { user, token, organization } = await server.services.superAdmin.adminSignUp({ ...req.body, ip: req.realIp, userAgent: req.headers["user-agent"] || "" @@ -110,6 +111,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { message: "Successfully set up admin account", user: user.user, token: token.access, + organization, new: "123" }; } diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 4ca4ef324..7441d58cb 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -9,7 +9,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Create identity", security: [ @@ -56,7 +56,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", url: "/:identityId", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Update identity", security: [ @@ -105,7 +105,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", url: "/:identityId", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Delete identity", security: [ diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 744ba6ebf..fbc68d974 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -48,6 +48,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await projectRouter.register(registerProjectMembershipRouter); await projectRouter.register(registerSecretTagRouter); }, + { prefix: "/workspace" } ); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index bfda652f0..6eff558fb 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -93,7 +93,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { .trim() .regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens") .optional(), - authEnforced: z.boolean().optional() + authEnforced: z.boolean().optional(), + scimEnabled: z.boolean().optional() }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index 09de72233..afa2135c0 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -1,6 +1,12 @@ import { z } from "zod"; -import { OrgMembershipsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { + OrgMembershipsSchema, + ProjectMembershipRole, + ProjectMembershipsSchema, + UserEncryptionKeysSchema, + UsersSchema +} from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -80,7 +86,10 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, - members: req.body.members + members: req.body.members.map((member) => ({ + ...member, + projectRole: ProjectMembershipRole.Member + })) }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index f0faaa0f4..0a5f8d85e 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -2,13 +2,11 @@ import { z } from "zod"; import { IntegrationsSchema, - ProjectKeysSchema, ProjectMembershipsSchema, ProjectsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -119,7 +117,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.getAProject({ actorId: req.permission.id, @@ -171,7 +169,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.deleteProject({ actorId: req.permission.id, @@ -216,6 +214,41 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + url: "/:workspaceId", + method: "PATCH", + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + name: z.string().trim().optional(), + autoCapitalization: z.boolean().optional() + }), + response: { + 200: z.object({ + workspace: ProjectsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const workspace = await server.services.project.updateProject({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + update: { + name: req.body.name, + autoCapitalization: req.body.autoCapitalization + } + }); + return { + workspace + }; + } + }); + server.route({ url: "/:workspaceId/auto-capitalization", method: "POST", @@ -249,48 +282,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - url: "/:workspaceId/invite-signup", - method: "POST", - schema: { - params: z.object({ - workspaceId: z.string().trim() - }), - body: z.object({ - email: z.string().trim() - }), - response: { - 200: z.object({ - invitee: UsersSchema, - latestKey: ProjectKeysSchema.optional() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { invitee, latestKey } = await server.services.projectMembership.inviteUserToProject({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - email: req.body.email - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: req.params.workspaceId, - event: { - type: EventType.ADD_WORKSPACE_MEMBER, - metadata: { - userId: invitee.id, - email: invitee.email - } - } - }); - return { invitee, latestKey }; - } - }); - server.route({ url: "/:workspaceId/integrations", method: "GET", diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts index 1f423c084..deebbc981 100644 --- a/backend/src/server/routes/v2/index.ts +++ b/backend/src/server/routes/v2/index.ts @@ -2,6 +2,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router"; import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerMfaRouter } from "./mfa-router"; import { registerOrgRouter } from "./organization-router"; +import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-router"; import { registerServiceTokenRouter } from "./service-token-router"; import { registerUserRouter } from "./user-router"; @@ -21,6 +22,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => { async (projectServer) => { await projectServer.register(registerProjectRouter); await projectServer.register(registerIdentityProjectRouter); + await projectServer.register(registerProjectMembershipRouter); }, { prefix: "/workspace" } ); diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts new file mode 100644 index 000000000..417fcb44c --- /dev/null +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -0,0 +1,95 @@ +import { z } from "zod"; + +import { ProjectMembershipsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/:projectId/memberships", + schema: { + params: z.object({ + projectId: z.string().describe("The ID of the project.") + }), + body: z.object({ + emails: z.string().email().array().describe("Emails of the users to add to the project.") + }), + response: { + 200: z.object({ + memberships: ProjectMembershipsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({ + projectId: req.params.projectId, + actorId: req.permission.id, + actor: req.permission.type, + emails: req.body.emails + }); + + await server.services.auditLog.createAuditLog({ + projectId: req.params.projectId, + ...req.auditLogInfo, + event: { + type: EventType.ADD_BATCH_WORKSPACE_MEMBER, + metadata: memberships.map(({ userId, id }) => ({ + userId: userId || "", + membershipId: id, + email: "" + })) + } + }); + + return { memberships }; + } + }); + + server.route({ + method: "DELETE", + url: "/:projectId/memberships", + schema: { + params: z.object({ + projectId: z.string().describe("The ID of the project.") + }), + + body: z.object({ + emails: z.string().email().array().describe("Emails of the users to remove from the project.") + }), + response: { + 200: z.object({ + memberships: ProjectMembershipsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const memberships = await server.services.projectMembership.deleteProjectMemberships({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + emails: req.body.emails + }); + + for (const membership of memberships) { + // eslint-disable-next-line no-await-in-loop + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.projectId, + event: { + type: EventType.REMOVE_WORKSPACE_MEMBER, + metadata: { + userId: membership.userId, + email: "" + } + } + }); + } + return { memberships }; + } + }); +}; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index d38efdbb9..7cbcff37c 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -1,11 +1,21 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { ProjectKeysSchema } from "@app/db/schemas"; +import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { authRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const projectWithEnv = ProjectsSchema.merge( + z.object({ + _id: z.string(), + environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() + }) +); + export const registerProjectRouter = async (server: FastifyZodProvider) => { + /* Get project key */ server.route({ url: "/:workspaceId/encrypted-key", method: "GET", @@ -34,8 +44,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { const key = await server.services.projectKey.getLatestProjectKey({ actor: req.permission.type, actorId: req.permission.id, - projectId: req.params.workspaceId, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId }); await server.services.auditLog.createAuditLog({ @@ -52,4 +62,97 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return key; } }); + + /* Start upgrade of a project */ + server.route({ + url: "/:projectId/upgrade", + method: "POST", + schema: { + params: z.object({ + projectId: z.string().trim() + }), + + body: z.object({ + userPrivateKey: z.string().trim() + }), + response: { + 200: z.void() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), + handler: async (req) => { + await server.services.project.upgradeProject({ + actorId: req.permission.id, + actor: req.permission.type, + projectId: req.params.projectId, + userPrivateKey: req.body.userPrivateKey + }); + } + }); + + /* Get upgrade status of project */ + server.route({ + url: "/:projectId/upgrade/status", + method: "GET", + schema: { + params: z.object({ + projectId: z.string().trim() + }), + response: { + 200: z.object({ + status: z.string().nullable() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), + handler: async (req) => { + const status = await server.services.project.getProjectUpgradeStatus({ + projectId: req.params.projectId, + actor: req.permission.type, + actorId: req.permission.id + }); + + return { status }; + } + }); + + /* Create new project */ + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + projectName: z.string().trim(), + slug: z + .string() + .min(5) + .max(36) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .optional(), + organizationId: z.string().trim() + }), + response: { + 200: z.object({ + project: projectWithEnv + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const project = await server.services.project.createProject({ + actorId: req.permission.id, + actor: req.permission.type, + orgId: req.body.organizationId, + workspaceName: req.body.projectName, + slug: req.body.slug + }); + + return { project }; + } + }); }; diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 8fc114e76..97bc3d864 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -197,7 +197,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), handler: async (req) => { const user = await server.services.user.getMe(req.permission.id); return { user }; diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 5f47ae3c5..1aac86205 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1092,7 +1092,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: z .object({ secretName: z.string().trim(), - type: z.nativeEnum(SecretType).default(SecretType.Shared), secretKeyCiphertext: z.string().trim(), secretKeyIV: z.string().trim(), secretKeyTag: z.string().trim(), @@ -1139,7 +1138,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { projectId, policy, data: { - [CommitType.Create]: inputSecrets.filter(({ type }) => type === "shared") + [CommitType.Create]: inputSecrets } }); diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index b04b88d2f..d1ec800f9 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -190,7 +190,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { const { user, accessToken, refreshToken } = await server.services.signup.completeAccountInvite({ ...req.body, ip: req.realIp, - userAgent + userAgent, + authorization: req.headers.authorization as string }); void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 04304980a..ee2cc5995 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -281,8 +281,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: name: "Oauth 2 login" }); } - - user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] }); + + user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], isGhost: false }); + } const isLinkingRequired = !user?.authMethods?.includes(authMethod); const isUserCompleted = user.isAccepted; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 752d5d563..61fcfe3ce 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -50,7 +50,7 @@ export const authSignupServiceFactory = ({ throw new Error("Failed to send verification code for complete account"); } if (!user) { - user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email }); + user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, isGhost: false }); } if (!user) throw new Error("Failed to create user"); @@ -212,13 +212,16 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, encryptedPrivateKeyIV, - encryptedPrivateKeyTag + encryptedPrivateKeyTag, + authorization }: TCompleteAccountInviteDTO) => { const user = await userDAL.findUserByEmail(email); if (!user || (user && user.isAccepted)) { throw new Error("Failed to complete account for complete user"); } + validateSignUpAuthorization(authorization, user.id); + const [orgMembership] = await orgDAL.findMembership({ inviteEmail: email, status: OrgMembershipStatus.Invited diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts index 69b779e8b..a37a1cd96 100644 --- a/backend/src/services/auth/auth-signup-type.ts +++ b/backend/src/services/auth/auth-signup-type.ts @@ -34,4 +34,5 @@ export type TCompleteAccountInviteDTO = { verifier: string; ip: string; userAgent: string; + authorization: string; }; diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index bea0dbe10..b051952e7 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -17,21 +17,24 @@ export enum AuthTokenType { API_KEY = "apiKey", SERVICE_ACCESS_TOKEN = "serviceAccessToken", SERVICE_REFRESH_TOKEN = "serviceRefreshToken", - IDENTITY_ACCESS_TOKEN = "identityAccessToken" + IDENTITY_ACCESS_TOKEN = "identityAccessToken", + SCIM_TOKEN = "scimToken" } export enum AuthMode { JWT = "jwt", SERVICE_TOKEN = "serviceToken", API_KEY = "apiKey", - IDENTITY_ACCESS_TOKEN = "identityAccessToken" + IDENTITY_ACCESS_TOKEN = "identityAccessToken", + SCIM_TOKEN = "scimToken" } export enum ActorType { // would extend to AWS, Azure, ... USER = "user", // userIdentity SERVICE = "service", IDENTITY = "identity", - Machine = "machine" + Machine = "machine", + SCIM_CLIENT = "scimClient" } export type AuthModeJwtTokenPayload = { diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index cdc8effe2..32774ccbb 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -35,12 +35,12 @@ export const identityAccessTokenServiceFactory = ({ } // ttl check - if (accessTokenTTL > 0) { + if (Number(accessTokenTTL) > 0) { const currentDate = new Date(); if (accessTokenLastRenewedAt) { // access token has been renewed const accessTokenRenewed = new Date(accessTokenLastRenewedAt); - const ttlInMilliseconds = accessTokenTTL * 1000; + const ttlInMilliseconds = Number(accessTokenTTL) * 1000; const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); if (currentDate > expirationDate) @@ -50,7 +50,7 @@ export const identityAccessTokenServiceFactory = ({ } else { // access token has never been renewed const accessTokenCreated = new Date(accessTokenCreatedAt); - const ttlInMilliseconds = accessTokenTTL * 1000; + const ttlInMilliseconds = Number(accessTokenTTL) * 1000; const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); if (currentDate > expirationDate) @@ -61,9 +61,9 @@ export const identityAccessTokenServiceFactory = ({ } // max ttl checks - if (accessTokenMaxTTL > 0) { + if (Number(accessTokenMaxTTL) > 0) { const accessTokenCreated = new Date(accessTokenCreatedAt); - const ttlInMilliseconds = accessTokenMaxTTL * 1000; + const ttlInMilliseconds = Number(accessTokenMaxTTL) * 1000; const currentDate = new Date(); const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); @@ -72,7 +72,7 @@ export const identityAccessTokenServiceFactory = ({ message: "Failed to renew MI access token due to Max TTL expiration" }); - const extendToDate = new Date(currentDate.getTime() + accessTokenTTL); + const extendToDate = new Date(currentDate.getTime() + Number(accessTokenTTL)); if (extendToDate > expirationDate) throw new UnauthorizedError({ message: "Failed to renew MI access token past its Max TTL expiration" diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 60e748ff7..3b73da6d4 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -69,9 +69,9 @@ export const identityUaServiceFactory = ({ if (!validClientSecretInfo) throw new UnauthorizedError(); const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo; - if (clientSecretTTL > 0) { + if (Number(clientSecretTTL) > 0) { const clientSecretCreated = new Date(validClientSecretInfo.createdAt); - const ttlInMilliseconds = clientSecretTTL * 1000; + const ttlInMilliseconds = Number(clientSecretTTL) * 1000; const currentDate = new Date(); const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds); @@ -124,7 +124,10 @@ export const identityUaServiceFactory = ({ } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, { - expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL + expiresIn: + Number(identityAccessToken.accessTokenMaxTTL) === 0 + ? undefined + : Number(identityAccessToken.accessTokenMaxTTL) } ); diff --git a/backend/src/services/integration-auth/integration-auth-dal.ts b/backend/src/services/integration-auth/integration-auth-dal.ts index f2d9c9ea2..d32cd1579 100644 --- a/backend/src/services/integration-auth/integration-auth-dal.ts +++ b/backend/src/services/integration-auth/integration-auth-dal.ts @@ -1,10 +1,35 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TIntegrationAuthDALFactory = ReturnType; export const integrationAuthDALFactory = (db: TDbClient) => { const integrationAuthOrm = ormify(db, TableName.IntegrationAuth); - return integrationAuthOrm; + + const bulkUpdate = async ( + data: Array<{ filter: Partial; data: TIntegrationAuthsUpdate }>, + tx?: Knex + ) => { + try { + const integrationAuths = await Promise.all( + data.map(async ({ filter, data: updateData }) => { + const [doc] = await (tx || db)(TableName.IntegrationAuth).where(filter).update(updateData).returning("*"); + if (!doc) throw new BadRequestError({ message: "Failed to update document" }); + return doc; + }) + ); + return integrationAuths; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + return { + ...integrationAuthOrm, + bulkUpdate + }; }; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index e914aac42..05c77e746 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -76,7 +76,8 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("publicKey").withSchema(TableName.UserEncryptionKey) - ); + ) + .where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({ ...data, user: { email, firstName, lastName, id: userId, publicKey } @@ -86,6 +87,79 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const findOrgMembersByEmail = async (orgId: string, emails: string[]) => { + try { + const members = await db(TableName.OrgMembership) + .where({ orgId }) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + db.ref("id").withSchema(TableName.OrgMembership), + db.ref("inviteEmail").withSchema(TableName.OrgMembership), + db.ref("orgId").withSchema(TableName.OrgMembership), + db.ref("role").withSchema(TableName.OrgMembership), + db.ref("roleId").withSchema(TableName.OrgMembership), + db.ref("status").withSchema(TableName.OrgMembership), + db.ref("email").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: false }) + .whereIn("email", emails); + return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({ + ...data, + user: { email, firstName, lastName, id: userId, publicKey } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find all org members" }); + } + }; + + const findOrgGhostUser = async (orgId: string) => { + try { + const member = await db(TableName.OrgMembership) + .where({ orgId }) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) + .select( + db.ref("id").withSchema(TableName.OrgMembership), + db.ref("orgId").withSchema(TableName.OrgMembership), + db.ref("role").withSchema(TableName.OrgMembership), + db.ref("roleId").withSchema(TableName.OrgMembership), + db.ref("status").withSchema(TableName.OrgMembership), + db.ref("email").withSchema(TableName.Users), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: true }) + .first(); + return member; + } catch (error) { + return null; + } + }; + + const ghostUserExists = async (orgId: string) => { + try { + const member = await db(TableName.OrgMembership) + .where({ orgId }) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) + .select(db.ref("id").withSchema(TableName.Users).as("userId")) + .where({ isGhost: true }) + .first(); + return Boolean(member); + } catch (error) { + return false; + } + }; + const create = async (dto: TOrganizationsInsert, tx?: Knex) => { try { const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*"); @@ -165,7 +239,14 @@ export const orgDALFactory = (db: TDbClient) => { // eslint-disable-next-line .where(buildFindFilter(filter)) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) - .select(selectAllTableCols(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users)); + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .select( + selectAllTableCols(TableName.OrgMembership), + db.ref("email").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users), + db.ref("scimEnabled").withSchema(TableName.Organization) + ); if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { @@ -184,6 +265,9 @@ export const orgDALFactory = (db: TDbClient) => { findAllOrgMembers, findOrgById, findAllOrgsByUserId, + ghostUserExists, + findOrgMembersByEmail, + findOrgGhostUser, create, updateById, deleteById, diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts new file mode 100644 index 000000000..ec6d4cb2d --- /dev/null +++ b/backend/src/services/org/org-fns.ts @@ -0,0 +1,41 @@ +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; + +type TDeleteOrgMembership = { + orgMembershipId: string; + orgId: string; + orgDAL: Pick; + projectDAL: Pick; + projectMembershipDAL: Pick; +}; + +export const deleteOrgMembership = async ({ + orgMembershipId, + orgId, + orgDAL, + projectDAL, + projectMembershipDAL +}: TDeleteOrgMembership) => { + const membership = await orgDAL.transaction(async (tx) => { + // delete org membership + const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); + + const projects = await projectDAL.find({ orgId }, { tx }); + + // delete associated project memberships + await projectMembershipDAL.delete( + { + $in: { + projectId: projects.map((project) => project.id) + }, + userId: orgMembership.userId as string + }, + tx + ); + + return orgMembership; + }); + + return membership; +}; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 7fafca438..f39ac5366 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -1,6 +1,8 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; +import crypto from "crypto"; import jwt from "jsonwebtoken"; +import { Knex } from "knex"; import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { TProjects } from "@app/db/schemas/projects"; @@ -11,6 +13,7 @@ import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config- import { getConfig } from "@app/lib/config/env"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; @@ -28,6 +31,7 @@ import { TOrgRoleDALFactory } from "./org-role-dal"; import { TDeleteOrgMembershipDTO, TFindAllWorkspacesDTO, + TFindOrgMembersByEmailDTO, TInviteUserToOrgDTO, TUpdateOrgDTO, TUpdateOrgMembershipDTO, @@ -93,6 +97,15 @@ export const orgServiceFactory = ({ return members; }; + const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + + const members = await orgDAL.findOrgMembersByEmail(orgId, emails); + + return members; + }; + const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); @@ -118,6 +131,54 @@ export const orgServiceFactory = ({ return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id)); }; + const addGhostUser = async (orgId: string, tx?: Knex) => { + const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. + const password = crypto.randomBytes(128).toString("hex"); + + const user = await userDAL.create( + { + isGhost: true, + authMethods: [AuthMethod.EMAIL], + email, + isAccepted: true + }, + tx + ); + + const encKeys = await generateUserSrpKeys(email, password); + + await userDAL.upsertUserEncryptionKey( + user.id, + { + encryptionVersion: 2, + protectedKey: encKeys.protectedKey, + protectedKeyIV: encKeys.protectedKeyIV, + protectedKeyTag: encKeys.protectedKeyTag, + publicKey: encKeys.publicKey, + encryptedPrivateKey: encKeys.encryptedPrivateKey, + iv: encKeys.encryptedPrivateKeyIV, + tag: encKeys.encryptedPrivateKeyTag, + salt: encKeys.salt, + verifier: encKeys.verifier + }, + tx + ); + + const createMembershipData = { + orgId, + userId: user.id, + role: OrgMembershipRole.Admin, + status: OrgMembershipStatus.Accepted + }; + + await orgDAL.createMembership(createMembershipData, tx); + + return { + user, + keys: encKeys + }; + }; + /* * Update organization details * */ @@ -126,16 +187,32 @@ export const orgServiceFactory = ({ actorId, actorOrgId, orgId, - data: { name, slug, authEnforced } + data: { name, slug, authEnforced, scimEnabled } }: TUpdateOrgDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + const plan = await licenseService.getPlan(orgId); + if (authEnforced !== undefined) { + if (!plan?.samlSSO) + throw new BadRequestError({ + message: + "Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO." + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); } - if (authEnforced) { + if (scimEnabled !== undefined) { + if (!plan?.scim) + throw new BadRequestError({ + message: + "Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning." + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); + } + + if (authEnforced || scimEnabled) { const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId); if (!samlCfg) throw new BadRequestError({ @@ -147,7 +224,8 @@ export const orgServiceFactory = ({ const org = await orgDAL.updateById(orgId, { name, slug: slug ? slugify(slug) : undefined, - authEnforced + authEnforced, + scimEnabled }); if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" }); return org; @@ -321,7 +399,8 @@ export const orgServiceFactory = ({ { email: inviteeEmail, isAccepted: false, - authMethods: [AuthMethod.EMAIL] + authMethods: [AuthMethod.EMAIL], + isGhost: false }, tx ); @@ -470,10 +549,12 @@ export const orgServiceFactory = ({ inviteUserToOrganization, verifyUserToOrg, updateOrg, + findOrgMembersByEmail, createOrganization, deleteOrganizationById, deleteOrgMembership, findAllWorkspaces, + addGhostUser, updateOrgMembership, // incident contacts findIncidentContacts, diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 01b3c8e37..bd8fe2e95 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -30,6 +30,13 @@ export type TVerifyUserToOrgDTO = { code: string; }; +export type TFindOrgMembersByEmailDTO = { + actor: ActorType; + actorId: string; + orgId: string; + emails: string[]; +}; + export type TFindAllWorkspacesDTO = { actor: ActorType; actorId: string; @@ -38,5 +45,5 @@ export type TFindAllWorkspacesDTO = { }; export type TUpdateOrgDTO = { - data: Partial<{ name: string; slug: string; authEnforced: boolean }>; + data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>; } & TOrgPermission; diff --git a/backend/src/services/project-bot/project-bot-dal.ts b/backend/src/services/project-bot/project-bot-dal.ts index 7f342f0ae..74abf8f21 100644 --- a/backend/src/services/project-bot/project-bot-dal.ts +++ b/backend/src/services/project-bot/project-bot-dal.ts @@ -27,5 +27,19 @@ export const projectBotDALFactory = (db: TDbClient) => { } }; - return { ...projectBotOrm, findOne }; + const findProjectByBotId = async (botId: string) => { + try { + const project = await db(TableName.ProjectBot) + .where({ [`${TableName.ProjectBot}.id` as "id"]: botId }) + .join(TableName.Project, `${TableName.ProjectBot}.projectId`, `${TableName.Project}.id`) + .select(selectAllTableCols(TableName.Project)) + .first(); + + return project || null; + } catch (error) { + throw new DatabaseError({ error, name: "Find project by bot id" }); + } + }; + + return { ...projectBotOrm, findOne, findProjectByBotId }; }; diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index 5ead160f2..456a1e7a0 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -1,125 +1,111 @@ import { ForbiddenError } from "@casl/ability"; -import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; +import { ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { getConfig } from "@app/lib/config/env"; -import { - decryptAsymmetric, - decryptSymmetric, - decryptSymmetric128BitHexKeyUTF8, - encryptSymmetric, - encryptSymmetric128BitHexKeyUTF8, - generateAsymmetricKeyPair -} from "@app/lib/crypto"; +import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto"; +import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { TProjectPermission } from "@app/lib/types"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotDALFactory } from "./project-bot-dal"; -import { TSetActiveStateDTO } from "./project-bot-types"; +import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types"; type TProjectBotServiceFactoryDep = { permissionService: Pick; + projectDAL: Pick; projectBotDAL: TProjectBotDALFactory; }; export type TProjectBotServiceFactory = ReturnType; -export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => { - const getBotKey = async (projectId: string) => { - const appCfg = getConfig(); - const encryptionKey = appCfg.ENCRYPTION_KEY; - const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; +export const projectBotServiceFactory = ({ + projectBotDAL, + projectDAL, + permissionService +}: TProjectBotServiceFactoryDep) => { + const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => + infisicalSymmetricDecrypt({ + keyEncoding: bot.keyEncoding as SecretKeyEncoding, + iv: bot.iv, + tag: bot.tag, + ciphertext: bot.encryptedPrivateKey + }); + const getBotKey = async (projectId: string) => { const bot = await projectBotDAL.findOne({ projectId }); if (!bot) throw new BadRequestError({ message: "failed to find bot key" }); if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" }); if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey) throw new BadRequestError({ message: "Encryption key missing" }); - if (rootEncryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.BASE64) { - const privateKeyBot = decryptSymmetric({ - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey, - key: rootEncryptionKey - }); - return decryptAsymmetric({ - ciphertext: bot.encryptedProjectKey, - privateKey: privateKeyBot, - nonce: bot.encryptedProjectKeyNonce, - publicKey: bot.sender.publicKey - }); - } - if (encryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.UTF8) { - const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey, - key: encryptionKey - }); - return decryptAsymmetric({ - ciphertext: bot.encryptedProjectKey, - privateKey: privateKeyBot, - nonce: bot.encryptedProjectKeyNonce, - publicKey: bot.sender.publicKey - }); - } + const botPrivateKey = getBotPrivateKey({ bot }); - throw new BadRequestError({ - message: "Failed to obtain bot copy of workspace key needed for operation" + return decryptAsymmetric({ + ciphertext: bot.encryptedProjectKey, + privateKey: botPrivateKey, + nonce: bot.encryptedProjectKeyNonce, + publicKey: bot.sender.publicKey }); }; - const findBotByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TProjectPermission) => { + const findBotByProjectId = async ({ + actorId, + actor, + projectId, + actorOrgId, + privateKey, + botKey, + publicKey + }: TFindBotByProjectIdDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); - const appCfg = getConfig(); const bot = await projectBotDAL.transaction(async (tx) => { const doc = await projectBotDAL.findOne({ projectId }, tx); if (doc) return doc; - const { publicKey, privateKey } = generateAsymmetricKeyPair(); - if (appCfg.ROOT_ENCRYPTION_KEY) { - const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY); - return projectBotDAL.create( - { - name: "Infisical Bot", - projectId, - tag, - iv, - encryptedPrivateKey: ciphertext, - isActive: false, - publicKey, - algorithm: SecretEncryptionAlgo.AES_256_GCM, - keyEncoding: SecretKeyEncoding.BASE64 - }, - tx - ); + const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair(); + + const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(keys.privateKey); + + const project = await projectDAL.findById(projectId, tx); + + if (project.version === ProjectVersion.V2) { + throw new BadRequestError({ message: "Failed to create bot, project is upgraded." }); } - if (appCfg.ENCRYPTION_KEY) { - const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(privateKey, appCfg.ENCRYPTION_KEY); - return projectBotDAL.create( - { - name: "Infisical Bot", - projectId, - tag, - iv, - encryptedPrivateKey: ciphertext, - isActive: false, - publicKey, - algorithm: SecretEncryptionAlgo.AES_256_GCM, - keyEncoding: SecretKeyEncoding.UTF8 - }, - tx - ); - } - throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" }); + + return projectBotDAL.create( + { + name: "Infisical Bot", + projectId, + tag, + iv, + encryptedPrivateKey: ciphertext, + isActive: false, + publicKey: keys.publicKey, + algorithm, + keyEncoding: encoding, + ...(botKey && { + encryptedProjectKey: botKey.encryptedKey, + encryptedProjectKeyNonce: botKey.nonce + }) + }, + tx + ); }); return bot; }; + const findProjectByBotId = async (botId: string) => { + try { + const bot = await projectBotDAL.findProjectByBotId(botId); + return bot; + } catch (e) { + throw new BadRequestError({ message: "Failed to find bot by ID" }); + } + }; + const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => { const bot = await projectBotDAL.findById(botId); if (!bot) throw new BadRequestError({ message: "Bot not found" }); @@ -127,6 +113,16 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T const { permission } = await permissionService.getProjectPermission(actor, actorId, bot.projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); + const project = await projectBotDAL.findProjectByBotId(botId); + + if (!project) { + throw new BadRequestError({ message: "Failed to find project by bot ID" }); + } + + if (project.version === ProjectVersion.V2) { + throw new BadRequestError({ message: "Failed to set bot active for upgraded project. Bot is already active" }); + } + if (isActive) { if (!botKey?.nonce || !botKey?.encryptedKey) { throw new BadRequestError({ message: "Failed to set bot active - missing bot key" }); @@ -153,6 +149,8 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T return { findBotByProjectId, setBotActiveState, + getBotPrivateKey, + findProjectByBotId, getBotKey }; }; diff --git a/backend/src/services/project-bot/project-bot-types.ts b/backend/src/services/project-bot/project-bot-types.ts index 94a2943eb..50fec2200 100644 --- a/backend/src/services/project-bot/project-bot-types.ts +++ b/backend/src/services/project-bot/project-bot-types.ts @@ -1,3 +1,4 @@ +import { TProjectBots } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TSetActiveStateDTO = { @@ -8,3 +9,16 @@ export type TSetActiveStateDTO = { }; botId: string; } & Omit; + +export type TFindBotByProjectIdDTO = { + privateKey?: string; + publicKey?: string; + botKey?: { + nonce: string; + encryptedKey: string; + }; +} & TProjectPermission; + +export type TGetPrivateKeyDTO = { + bot: TProjectBots; +}; diff --git a/backend/src/services/project-key/project-key-dal.ts b/backend/src/services/project-key/project-key-dal.ts index 7423a48de..d1b4053d0 100644 --- a/backend/src/services/project-key/project-key-dal.ts +++ b/backend/src/services/project-key/project-key-dal.ts @@ -1,3 +1,5 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName, TProjectKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; @@ -10,10 +12,11 @@ export const projectKeyDALFactory = (db: TDbClient) => { const findLatestProjectKey = async ( userId: string, - projectId: string + projectId: string, + tx?: Knex ): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => { try { - const projectKey = await db(TableName.ProjectKeys) + const projectKey = await (tx || db)(TableName.ProjectKeys) .join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`) .join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) .where({ projectId, receiverId: userId }) @@ -29,9 +32,9 @@ export const projectKeyDALFactory = (db: TDbClient) => { } }; - const findAllProjectUserPubKeys = async (projectId: string) => { + const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => { try { - const pubKeys = await db(TableName.ProjectMembership) + const pubKeys = await (tx || db)(TableName.ProjectMembership) .where({ projectId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index 22b9937a9..e0002f911 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -1,7 +1,7 @@ import { TDbClient } from "@app/db"; import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TProjectMembershipDALFactory = ReturnType; @@ -24,20 +24,63 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("projectId").withSchema(TableName.ProjectMembership), db.ref("role").withSchema(TableName.ProjectMembership), db.ref("roleId").withSchema(TableName.ProjectMembership), + db.ref("isGhost").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), db.ref("publicKey").withSchema(TableName.UserEncryptionKey), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId") - ); - return members.map(({ email, firstName, lastName, publicKey, ...data }) => ({ + ) + .where({ isGhost: false }); + return members.map(({ email, firstName, lastName, publicKey, isGhost, ...data }) => ({ ...data, - user: { email, firstName, lastName, id: data.userId, publicKey } + user: { email, firstName, lastName, id: data.userId, publicKey, isGhost } })); } catch (error) { throw new DatabaseError({ error, name: "Find all project members" }); } }; - return { ...projectMemberOrm, findAllProjectMembers }; + const findProjectGhostUser = async (projectId: string) => { + try { + const ghostUser = await db(TableName.ProjectMembership) + .where({ projectId }) + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .select(selectAllTableCols(TableName.Users)) + .where({ isGhost: true }) + .first(); + + return ghostUser; + } catch (error) { + throw new DatabaseError({ error, name: "Find project top-level user" }); + } + }; + + const findMembershipsByEmail = async (projectId: string, emails: string[]) => { + try { + const members = await db(TableName.ProjectMembership) + .where({ projectId }) + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .join( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + selectAllTableCols(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("email").withSchema(TableName.Users) + ) + .whereIn("email", emails) + .where({ isGhost: false }); + return members.map(({ userId, email, ...data }) => ({ + ...data, + user: { id: userId, email } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find members by email" }); + } + }; + + return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail }; }; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 6b2123e5e..e96247c97 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -1,15 +1,28 @@ +/* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; -import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas"; +import { + OrgMembershipStatus, + ProjectMembershipRole, + ProjectVersion, + SecretKeyEncoding, + TableName, + TProjectMemberships, + TUsers +} from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; +import { ActorType } from "../auth/auth-type"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; +import { assignWorkspaceKeysToMembers } from "../project/project-fns"; +import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; @@ -17,7 +30,9 @@ import { TUserDALFactory } from "../user/user-dal"; import { TProjectMembershipDALFactory } from "./project-membership-dal"; import { TAddUsersToWorkspaceDTO, - TDeleteProjectMembershipDTO, + TAddUsersToWorkspaceNonE2EEDTO, + TDeleteProjectMembershipOldDTO, + TDeleteProjectMembershipsDTO, TGetProjectMembershipDTO, TInviteUserToProjectDTO, TUpdateProjectMembershipDTO @@ -26,11 +41,12 @@ import { type TProjectMembershipServiceFactoryDep = { permissionService: Pick; smtpService: TSmtpService; + projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; - userDAL: Pick; + userDAL: Pick; projectRoleDAL: Pick; - orgDAL: Pick; - projectDAL: Pick; + orgDAL: Pick; + projectDAL: Pick; projectKeyDAL: Pick; licenseService: Pick; }; @@ -42,6 +58,7 @@ export const projectMembershipServiceFactory = ({ projectMembershipDAL, smtpService, projectRoleDAL, + projectBotDAL, orgDAL, userDAL, projectDAL, @@ -55,64 +72,90 @@ export const projectMembershipServiceFactory = ({ return projectMembershipDAL.findAllProjectMembers(projectId); }; - const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, email }: TInviteUserToProjectDTO) => { + const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, emails }: TInviteUserToProjectDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); - const invitee = await userDAL.findOne({ email }); - if (!invitee || !invitee.isAccepted) - throw new BadRequestError({ - message: "Faield to validate invitee", - name: "Invite user to project" - }); - - const inviteeMembership = await projectMembershipDAL.findOne({ - userId: invitee.id, - projectId - }); - if (inviteeMembership) - throw new BadRequestError({ - message: "Existing member of project", - name: "Invite user to project" - }); + const invitees: TUsers[] = []; const project = await projectDAL.findById(projectId); - const inviteeMembershipOrg = await orgDAL.findMembership({ - userId: invitee.id, - orgId: project.orgId, - status: OrgMembershipStatus.Accepted + const users = await userDAL.find({ + $in: { email: emails } }); - if (!inviteeMembershipOrg) - throw new BadRequestError({ - message: "Failed to validate invitee org membership", - name: "Invite user to project" + + await projectDAL.transaction(async (tx) => { + for (const invitee of users) { + if (!invitee.isAccepted) + throw new BadRequestError({ + message: "Failed to validate invitee", + name: "Invite user to project" + }); + + const inviteeMembership = await projectMembershipDAL.findOne( + { + userId: invitee.id, + projectId + }, + tx + ); + + if (inviteeMembership) { + throw new BadRequestError({ + message: "Existing member of project", + name: "Invite user to project" + }); + } + + const inviteeMembershipOrg = await orgDAL.findMembership({ + userId: invitee.id, + orgId: project.orgId, + status: OrgMembershipStatus.Accepted + }); + + if (!inviteeMembershipOrg) { + throw new BadRequestError({ + message: "Failed to validate invitee org membership", + name: "Invite user to project" + }); + } + + await projectMembershipDAL.create( + { + userId: invitee.id, + projectId, + role: ProjectMembershipRole.Member + }, + tx + ); + + invitees.push(invitee); + } + + const appCfg = getConfig(); + await smtpService.sendMail({ + template: SmtpTemplates.WorkspaceInvite, + subjectLine: "Infisical workspace invitation", + recipients: invitees.map((i) => i.email), + substitutions: { + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } }); + }); const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId); - await projectMembershipDAL.create({ - userId: invitee.id, - projectId, - role: ProjectMembershipRole.Member - }); - const sender = await userDAL.findById(actorId); - const appCfg = getConfig(); - await smtpService.sendMail({ - template: SmtpTemplates.WorkspaceInvite, - subjectLine: "Infisical workspace invitation", - recipients: [invitee.email], - substitutions: { - inviterFirstName: sender.firstName, - inviterEmail: sender.email, - workspaceName: project.name, - callback_url: `${appCfg.SITE_URL}/login` - } - }); - - return { invitee, latestKey }; + return { invitees, latestKey }; }; - const addUsersToProject = async ({ projectId, actorId, actor, actorOrgId, members }: TAddUsersToWorkspaceDTO) => { + const addUsersToProject = async ({ + projectId, + actorId, + actor, + actorOrgId, + members, + sendEmails = true + }: TAddUsersToWorkspaceDTO) => { const project = await projectDAL.findById(projectId); if (!project) throw new BadRequestError({ message: "Project not found" }); @@ -134,11 +177,16 @@ export const projectMembershipServiceFactory = ({ await projectMembershipDAL.transaction(async (tx) => { await projectMembershipDAL.insertMany( - orgMembers.map(({ userId }) => ({ - projectId, - userId: userId as string, - role: ProjectMembershipRole.Member - })), + orgMembers.map(({ userId, id: membershipId }) => { + const role = + members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member; + + return { + projectId, + userId: userId as string, + role + }; + }), tx ); const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); @@ -153,22 +201,132 @@ export const projectMembershipServiceFactory = ({ tx ); }); - const sender = await userDAL.findById(actorId); - const appCfg = getConfig(); - await smtpService.sendMail({ - template: SmtpTemplates.WorkspaceInvite, - subjectLine: "Infisical workspace invitation", - recipients: orgMembers.map(({ email }) => email).filter(Boolean), - substitutions: { - inviterFirstName: sender.firstName, - inviterEmail: sender.email, - workspaceName: project.name, - callback_url: `${appCfg.SITE_URL}/login` - } - }); + + if (sendEmails) { + const appCfg = getConfig(); + await smtpService.sendMail({ + template: SmtpTemplates.WorkspaceInvite, + subjectLine: "Infisical workspace invitation", + recipients: orgMembers.map(({ email }) => email).filter(Boolean), + substitutions: { + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } + }); + } return orgMembers; }; + const addUsersToProjectNonE2EE = async ({ + projectId, + actorId, + actor, + emails, + sendEmails = true + }: TAddUsersToWorkspaceNonE2EEDTO) => { + const project = await projectDAL.findById(projectId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + + if (project.version === ProjectVersion.V1) { + throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); + } + + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + + const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails); + + if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" }); + + const existingMembers = await projectMembershipDAL.find({ + projectId, + $in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) } + }); + if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" }); + + const ghostUser = await projectDAL.findProjectGhostUser(projectId); + + if (!ghostUser) { + throw new BadRequestError({ + message: "Failed to find sudo user" + }); + } + + const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId); + + if (!ghostUserLatestKey) { + throw new BadRequestError({ + message: "Failed to find sudo user latest key" + }); + } + + const bot = await projectBotDAL.findOne({ projectId }); + + if (!bot) { + throw new BadRequestError({ + message: "Failed to find bot" + }); + } + + const botPrivateKey = infisicalSymmetricDecrypt({ + keyEncoding: bot.keyEncoding as SecretKeyEncoding, + iv: bot.iv, + tag: bot.tag, + ciphertext: bot.encryptedPrivateKey + }); + + const newWsMembers = assignWorkspaceKeysToMembers({ + decryptKey: ghostUserLatestKey, + userPrivateKey: botPrivateKey, + members: orgMembers.map((membership) => ({ + orgMembershipId: membership.id, + projectMembershipRole: ProjectMembershipRole.Member, + userPublicKey: membership.user.publicKey + })) + }); + + const members: TProjectMemberships[] = []; + + await projectMembershipDAL.transaction(async (tx) => { + const result = await projectMembershipDAL.insertMany( + orgMembers.map(({ user }) => ({ + projectId, + userId: user.id, + role: ProjectMembershipRole.Member + })), + tx + ); + + members.push(...result); + + const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId); + await projectKeyDAL.insertMany( + orgMembers.map(({ user, id }) => ({ + encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, + nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, + senderId: ghostUser.id, + receiverId: user.id, + projectId + })), + tx + ); + }); + + if (sendEmails) { + const appCfg = getConfig(); + await smtpService.sendMail({ + template: SmtpTemplates.WorkspaceInvite, + subjectLine: "Infisical workspace invitation", + recipients: orgMembers.map(({ user }) => user.email).filter(Boolean), + substitutions: { + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } + }); + } + return members; + }; + const updateProjectMembership = async ({ actorId, actor, @@ -180,6 +338,15 @@ export const projectMembershipServiceFactory = ({ const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); + + if (membershipUser?.isGhost) { + throw new BadRequestError({ + message: "Unauthorized member update", + name: "Update project membership" + }); + } + const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); if (isCustomRole) { const customRole = await projectRoleDAL.findOne({ slug: role, projectId }); @@ -205,16 +372,26 @@ export const projectMembershipServiceFactory = ({ return membership; }; + // This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now. const deleteProjectMembership = async ({ actorId, actor, actorOrgId, projectId, membershipId - }: TDeleteProjectMembershipDTO) => { + }: TDeleteProjectMembershipOldDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + const member = await userDAL.findUserByProjectMembershipId(membershipId); + + if (member?.isGhost) { + throw new BadRequestError({ + message: "Unauthorized member delete", + name: "Delete project membership" + }); + } + const membership = await projectMembershipDAL.transaction(async (tx) => { const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx); await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx); @@ -223,11 +400,78 @@ export const projectMembershipServiceFactory = ({ return membership; }; + const deleteProjectMemberships = async ({ + actorId, + actor, + actorOrgId, + projectId, + emails + }: TDeleteProjectMembershipsDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + + const project = await projectDAL.findById(projectId); + + if (!project) { + throw new BadRequestError({ + message: "Project not found", + name: "Delete project membership" + }); + } + + if (project.version === ProjectVersion.V1) { + throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); + } + + const projectMembers = await projectMembershipDAL.findMembershipsByEmail(projectId, emails); + + if (projectMembers.length !== emails.length) { + throw new BadRequestError({ + message: "Some users are not part of project", + name: "Delete project membership" + }); + } + + if (actor === ActorType.USER && projectMembers.some(({ user }) => user.id === actorId)) { + throw new BadRequestError({ + message: "Cannot remove yourself from project", + name: "Delete project membership" + }); + } + + const memberships = await projectMembershipDAL.transaction(async (tx) => { + const deletedMemberships = await projectMembershipDAL.delete( + { + projectId, + $in: { + id: projectMembers.map(({ id }) => id) + } + }, + tx + ); + + await projectKeyDAL.delete( + { + projectId, + $in: { + receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean) + } + }, + tx + ); + + return deletedMemberships; + }); + return memberships; + }; + return { getProjectMemberships, inviteUserToProject, updateProjectMembership, - deleteProjectMembership, + addUsersToProjectNonE2EE, + deleteProjectMemberships, + deleteProjectMembership, // TODO: Remove this addUsersToProject }; }; diff --git a/backend/src/services/project-membership/project-membership-types.ts b/backend/src/services/project-membership/project-membership-types.ts index 6ee1d2965..634b51d77 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -1,9 +1,10 @@ +import { ProjectMembershipRole } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TGetProjectMembershipDTO = TProjectPermission; export type TInviteUserToProjectDTO = { - email: string; + emails: string[]; } & TProjectPermission; export type TUpdateProjectMembershipDTO = { @@ -11,14 +12,25 @@ export type TUpdateProjectMembershipDTO = { role: string; } & TProjectPermission; -export type TDeleteProjectMembershipDTO = { +export type TDeleteProjectMembershipOldDTO = { membershipId: string; } & TProjectPermission; +export type TDeleteProjectMembershipsDTO = { + emails: string[]; +} & TProjectPermission; + export type TAddUsersToWorkspaceDTO = { + sendEmails?: boolean; members: { orgMembershipId: string; workspaceEncryptedKey: string; workspaceEncryptedNonce: string; + projectRole: ProjectMembershipRole; }[]; } & TProjectPermission; + +export type TAddUsersToWorkspaceNonE2EEDTO = { + sendEmails?: boolean; + emails: string[]; +} & TProjectPermission; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 44ba57481..7d0826e12 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -1,6 +1,8 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { ProjectsSchema, TableName } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; +import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TProjectDALFactory = ReturnType; @@ -52,6 +54,32 @@ export const projectDALFactory = (db: TDbClient) => { } }; + const findProjectGhostUser = async (projectId: string) => { + try { + const ghostUser = await db(TableName.ProjectMembership) + .where({ projectId }) + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .select(selectAllTableCols(TableName.Users)) + .where({ isGhost: true }) + .first(); + return ghostUser; + } catch (error) { + throw new DatabaseError({ error, name: "Find project top-level user" }); + } + }; + + const setProjectUpgradeStatus = async (projectId: string, status: ProjectUpgradeStatus | null, tx?: Knex) => { + try { + const data: TProjectsUpdate = { + upgradeStatus: status + } as const; + + await (tx || db)(TableName.Project).where({ id: projectId }).update(data); + } catch (error) { + throw new DatabaseError({ error, name: "Set project upgrade status" }); + } + }; + const findAllProjectsByIdentity = async (identityId: string) => { try { const workspaces = await db(TableName.IdentityProjectMembership) @@ -132,10 +160,25 @@ export const projectDALFactory = (db: TDbClient) => { } }; + const checkProjectUpgradeStatus = async (projectId: string) => { + const project = await projectOrm.findById(projectId); + const upgradeInProgress = + project.upgradeStatus === ProjectUpgradeStatus.InProgress && project.version === ProjectVersion.V1; + + if (upgradeInProgress) { + throw new BadRequestError({ + message: "Project is currently being upgraded, and secrets cannot be written. Please try again" + }); + } + }; + return { ...projectOrm, findAllProjects, + setProjectUpgradeStatus, findAllProjectsByIdentity, - findProjectById + findProjectGhostUser, + findProjectById, + checkProjectUpgradeStatus }; }; diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts new file mode 100644 index 000000000..0f1f5a08d --- /dev/null +++ b/backend/src/services/project/project-fns.ts @@ -0,0 +1,50 @@ +import crypto from "crypto"; + +import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; + +import { AddUserToWsDTO } from "./project-types"; + +export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => { + const plaintextProjectKey = decryptAsymmetric({ + ciphertext: decryptKey.encryptedKey, + nonce: decryptKey.nonce, + publicKey: decryptKey.sender.publicKey, + privateKey: userPrivateKey + }); + + const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => { + const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric( + plaintextProjectKey, + userPublicKey, + userPrivateKey + ); + + return { + orgMembershipId, + projectRole: projectMembershipRole, + workspaceEncryptedKey: inviteeCipherText, + workspaceEncryptedNonce: inviteeNonce + }; + }); + + return newWsMembers; +}; + +type TCreateProjectKeyDTO = { + publicKey: string; + privateKey: string; +}; + +export const createProjectKey = ({ publicKey, privateKey }: TCreateProjectKeyDTO) => { + // 3. Create a random key that we'll use as the project key. + const randomBytes = crypto.randomBytes(16).toString("hex"); + + // 4. Encrypt the project key with the users key pair. + const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric( + randomBytes, + publicKey, + privateKey + ); + + return { key: encryptedProjectKey, iv: encryptedProjectKeyIv }; +}; diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts new file mode 100644 index 000000000..616aa1aef --- /dev/null +++ b/backend/src/services/project/project-queue.ts @@ -0,0 +1,541 @@ +/* eslint-disable no-await-in-loop */ +import { + IntegrationAuthsSchema, + ProjectMembershipRole, + ProjectUpgradeStatus, + ProjectVersion, + SecretApprovalRequestsSecretsSchema, + SecretKeyEncoding, + SecretsSchema, + SecretVersionsSchema, + TIntegrationAuths, + TSecretApprovalRequestsSecrets, + TSecrets, + TSecretVersions +} from "@app/db/schemas"; +import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; +import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; +import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; +import { + decryptIntegrationAuths, + decryptSecretApprovals, + decryptSecrets, + decryptSecretVersions +} from "@app/lib/crypto"; +import { + decryptAsymmetric, + encryptSymmetric128BitHexKeyUTF8, + infisicalSymmetricDecrypt, + infisicalSymmetricEncypt +} from "@app/lib/crypto/encryption"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue"; + +import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TOrgServiceFactory } from "../org/org-service"; +import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; +import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TSecretDALFactory } from "../secret/secret-dal"; +import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; +import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; +import { TUserDALFactory } from "../user/user-dal"; +import { TProjectDALFactory } from "./project-dal"; +import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; + +export type TProjectQueueFactory = ReturnType; + +type TProjectQueueFactoryDep = { + queueService: TQueueServiceFactory; + secretVersionDAL: Pick; + folderDAL: Pick; + secretDAL: Pick; + projectKeyDAL: Pick; + secretApprovalRequestDAL: Pick; + secretApprovalSecretDAL: Pick; + projectBotDAL: Pick; + orgService: Pick; + projectMembershipDAL: Pick; + integrationAuthDAL: TIntegrationAuthDALFactory; + userDAL: Pick; + + projectEnvDAL: Pick; + projectDAL: Pick; + orgDAL: Pick; +}; + +export const projectQueueFactory = ({ + queueService, + secretDAL, + folderDAL, + userDAL, + secretVersionDAL, + integrationAuthDAL, + secretApprovalRequestDAL, + secretApprovalSecretDAL, + projectKeyDAL, + projectBotDAL, + projectEnvDAL, + orgDAL, + projectDAL, + orgService, + projectMembershipDAL +}: TProjectQueueFactoryDep) => { + const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => { + await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, { + attempts: 1, + removeOnComplete: true, + removeOnFail: { + count: 5 // keep the most recent jobs + } + }); + }; + + queueService.start(QueueName.UpgradeProjectToGhost, async ({ data }) => { + try { + const [project] = await projectDAL.find({ + id: data.projectId, + version: ProjectVersion.V1 + }); + + const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId); + + if (!project || !oldProjectKey) { + throw new Error("Project or project key not found"); + } + + if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) { + throw new Error("Project upgrade status is not valid"); + } + + await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.InProgress); // Set the status to in progress. This is important to prevent multiple upgrades at the same time. + + // eslint-disable-next-line no-promise-executor-return + // await new Promise((resolve) => setTimeout(resolve, 50_000)); + + const userPrivateKey = infisicalSymmetricDecrypt({ + keyEncoding: data.encryptedPrivateKey.keyEncoding, + ciphertext: data.encryptedPrivateKey.encryptedKey, + iv: data.encryptedPrivateKey.encryptedKeyIv, + tag: data.encryptedPrivateKey.encryptedKeyTag + }); + + const projectEnvs = await projectEnvDAL.find({ + projectId: project.id + }); + + const projectFolders = await folderDAL.find({ + $in: { + envId: projectEnvs.map((env) => env.id) + } + }); + + // Get all the secrets within the project (as encrypted) + const projectIntegrationAuths = await integrationAuthDAL.find({ + projectId: project.id + }); + const secrets: TSecrets[] = []; + const secretVersions: TSecretVersions[] = []; + const approvalSecrets: TSecretApprovalRequestsSecrets[] = []; + const folderSecretVersionIdsToDelete: string[] = []; + + for (const folder of projectFolders) { + const folderSecrets = await secretDAL.find({ folderId: folder.id }); + + const folderSecretVersions = await secretVersionDAL.find( + { + folderId: folder.id + }, + // Only get the latest 700 secret versions for each folder. + { + limit: 1000, + sort: [["createdAt", "desc"]] + } + ); + + const deletedSecretVersions = await secretVersionDAL.find( + { + folderId: folder.id + }, + { + // Get all the secret versions that are not the latest 700 + offset: 1000 + } + ); + folderSecretVersionIdsToDelete.push(...deletedSecretVersions.map((el) => el.id)); + + const approvalRequests = await secretApprovalRequestDAL.find({ + status: RequestState.Open, + folderId: folder.id + }); + const secretApprovals = await secretApprovalSecretDAL.find({ + $in: { + requestId: approvalRequests.map((el) => el.id) + } + }); + + secrets.push(...folderSecrets); + secretVersions.push(...folderSecretVersions); + approvalSecrets.push(...secretApprovals); + } + + const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey); + const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey); + const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey); + const decryptedIntegrationAuths = decryptIntegrationAuths(projectIntegrationAuths, userPrivateKey, oldProjectKey); + + // Get the existing bot and the existing project keys for the members of the project + const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null); + const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id }); + + // TRANSACTION START + await projectDAL.transaction(async (tx) => { + await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx); + + // Create a ghost user + const ghostUser = await orgService.addGhostUser(project.orgId, tx); + + // Create a project key + const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({ + publicKey: ghostUser.keys.publicKey, + privateKey: ghostUser.keys.plainPrivateKey + }); + + // Create a new project key for the GHOST + await projectKeyDAL.create( + { + projectId: project.id, + receiverId: ghostUser.user.id, + encryptedKey: newEncryptedProjectKey, + nonce: newEncryptedProjectKeyIv, + senderId: ghostUser.user.id + }, + tx + ); + + // Create a membership for the ghost user + await projectMembershipDAL.create( + { + projectId: project.id, + userId: ghostUser.user.id, + role: ProjectMembershipRole.Admin + }, + tx + ); + + // If a bot already exists, delete it + if (existingBot) { + await projectBotDAL.delete({ id: existingBot.id }, tx); + } + + // Delete all the existing project keys + await projectKeyDAL.delete( + { + projectId: project.id, + $in: { + id: existingProjectKeys.map((key) => key.id) + } + }, + tx + ); + + const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx); + + if (!ghostUserLatestKey) { + throw new Error("User latest key not found (V2 Upgrade)"); + } + + const newProjectMembers: { + encryptedKey: string; + nonce: string; + senderId: string; + receiverId: string; + projectId: string; + }[] = []; + + for (const key of existingProjectKeys) { + const user = await userDAL.findUserEncKeyByUserId(key.receiverId); + const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId }); + + if (!user || !orgMembership) { + throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`); + } + + const [newMember] = assignWorkspaceKeysToMembers({ + decryptKey: ghostUserLatestKey, + userPrivateKey: ghostUser.keys.plainPrivateKey, + members: [ + { + userPublicKey: user.publicKey, + orgMembershipId: orgMembership.id, + projectMembershipRole: ProjectMembershipRole.Admin + } + ] + }); + + newProjectMembers.push({ + encryptedKey: newMember.workspaceEncryptedKey, + nonce: newMember.workspaceEncryptedNonce, + senderId: ghostUser.user.id, + receiverId: user.id, + projectId: project.id + }); + } + + // Create project keys for all the old members + await projectKeyDAL.insertMany(newProjectMembers, tx); + + // Encrypt the bot private key (which is the same as the ghost user) + const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey); + + // 5. Create a bot for the project + const newBot = await projectBotDAL.create( + { + name: "Infisical Bot (Ghost)", + projectId: project.id, + tag, + iv, + encryptedPrivateKey: ciphertext, + isActive: true, + publicKey: ghostUser.keys.publicKey, + senderId: ghostUser.user.id, + encryptedProjectKey: newEncryptedProjectKey, + encryptedProjectKeyNonce: newEncryptedProjectKeyIv, + algorithm, + keyEncoding: encoding + }, + tx + ); + + const botPrivateKey = infisicalSymmetricDecrypt({ + keyEncoding: newBot.keyEncoding as SecretKeyEncoding, + iv: newBot.iv, + tag: newBot.tag, + ciphertext: newBot.encryptedPrivateKey + }); + + const botKey = decryptAsymmetric({ + ciphertext: newBot.encryptedProjectKey!, + privateKey: botPrivateKey, + nonce: newBot.encryptedProjectKeyNonce!, + publicKey: ghostUser.keys.publicKey + }); + + const updatedSecrets: TSecrets[] = []; + const updatedSecretVersions: TSecretVersions[] = []; + const updatedSecretApprovals: TSecretApprovalRequestsSecrets[] = []; + const updatedIntegrationAuths: TIntegrationAuths[] = []; + for (const rawSecret of decryptedSecrets) { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretValue || "", botKey); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecret.decrypted.secretComment || "", + botKey + ); + + const payload: TSecrets = { + ...rawSecret.original, + keyEncoding: SecretKeyEncoding.UTF8, + + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, + + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag + } as const; + + if (!SecretsSchema.safeParse(payload).success) { + throw new Error(`Invalid secret payload: ${JSON.stringify(payload)}`); + } + + updatedSecrets.push(payload); + } + + for (const rawSecretVersion of decryptedSecretVersions) { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretVersion.decrypted.secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretVersion.decrypted.secretValue || "", + botKey + ); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretVersion.decrypted.secretComment || "", + botKey + ); + + const payload: TSecretVersions = { + ...rawSecretVersion.original, + keyEncoding: SecretKeyEncoding.UTF8, + + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, + + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag + } as const; + + if (!SecretVersionsSchema.safeParse(payload).success) { + throw new Error(`Invalid secret version payload: ${JSON.stringify(payload)}`); + } + + updatedSecretVersions.push(payload); + } + + for (const rawSecretApproval of decryptedApprovalSecrets) { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretApproval.decrypted.secretKey, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretApproval.decrypted.secretValue || "", + botKey + ); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8( + rawSecretApproval.decrypted.secretComment || "", + botKey + ); + + const payload: TSecretApprovalRequestsSecrets = { + ...rawSecretApproval.original, + keyEncoding: SecretKeyEncoding.UTF8, + + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, + + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag + } as const; + + if (!SecretApprovalRequestsSecretsSchema.safeParse(payload).success) { + throw new Error(`Invalid secret approval payload: ${JSON.stringify(payload)}`); + } + + updatedSecretApprovals.push(payload); + } + + for (const integrationAuth of decryptedIntegrationAuths) { + const access = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.access, botKey); + const accessId = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.accessId, botKey); + const refresh = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.refresh, botKey); + + const payload: TIntegrationAuths = { + ...integrationAuth.original, + keyEncoding: SecretKeyEncoding.UTF8, + + accessCiphertext: access.ciphertext, + accessIV: access.iv, + accessTag: access.tag, + + accessIdCiphertext: accessId.ciphertext, + accessIdIV: accessId.iv, + accessIdTag: accessId.tag, + + refreshCiphertext: refresh.ciphertext, + refreshIV: refresh.iv, + refreshTag: refresh.tag + } as const; + + if (!IntegrationAuthsSchema.safeParse(payload).success) { + throw new Error(`Invalid integration auth payload: ${JSON.stringify(payload)}`); + } + + updatedIntegrationAuths.push(payload); + } + + if (updatedSecrets.length !== secrets.length) { + throw new Error("Failed to update some secrets"); + } + if (updatedSecretVersions.length !== secretVersions.length) { + throw new Error("Failed to update some secret versions"); + } + if (updatedSecretApprovals.length !== approvalSecrets.length) { + throw new Error("Failed to update some secret approvals"); + } + if (updatedIntegrationAuths.length !== projectIntegrationAuths.length) { + throw new Error("Failed to update some integration auths"); + } + + const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(updatedSecrets, tx); + const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(updatedSecretVersions, tx); + const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement( + updatedSecretApprovals, + tx + ); + const integrationAuthUpdates = await integrationAuthDAL.bulkUpdate( + updatedIntegrationAuths.map((el) => ({ + filter: { id: el.id }, + data: { + ...el, + id: undefined + } + })), + tx + ); + + // Delete all secret versions that are no longer needed. We only store the latest 100 versions for each secret. + await secretVersionDAL.delete( + { + $in: { + id: folderSecretVersionIdsToDelete + } + }, + tx + ); + + if ( + secretUpdates.length !== updatedSecrets.length || + secretVersionUpdates.length !== updatedSecretVersions.length || + secretApprovalUpdates.length !== updatedSecretApprovals.length || + integrationAuthUpdates.length !== updatedIntegrationAuths.length + ) { + throw new Error("Parts of the upgrade failed. Some secrets were not updated"); + } + + await projectDAL.setProjectUpgradeStatus(data.projectId, null, tx); + + // await new Promise((resolve) => setTimeout(resolve, 15_000)); + // throw new Error("Transaction was successful!"); + }); + } catch (err) { + const [project] = await projectDAL + .find({ + id: data.projectId, + version: ProjectVersion.V1 + }) + .catch(() => [null]); + + if (!project) { + logger.error("Failed to upgrade project, because no project was found", data); + } else { + await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed); + logger.error(err, "Failed to upgrade project"); + } + + throw err; + } + }); + + queueService.listen(QueueName.UpgradeProjectToGhost, "failed", (job, err) => { + logger.error(err, "Upgrade project failed", job?.data); + }); + + return { + upgradeProject + }; +}; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 387c9bd78..c7c3af6fa 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,22 +1,40 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { ProjectMembershipRole } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { createSecretBlindIndex } from "@app/lib/crypto"; -import { BadRequestError } from "@app/lib/errors"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { TProjectPermission } from "@app/lib/types"; +import { ActorType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; +import { TOrgServiceFactory } from "../org/org-service"; +import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; +import { TUserDALFactory } from "../user/user-dal"; import { TProjectDALFactory } from "./project-dal"; -import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types"; +import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; +import { TProjectQueueFactory } from "./project-queue"; +import { + TCreateProjectDTO, + TDeleteProjectDTO, + TGetProjectDTO, + TUpdateProjectDTO, + TUpgradeProjectDTO +} from "./project-types"; export const DEFAULT_PROJECT_ENVS = [ { name: "Development", slug: "dev" }, @@ -26,11 +44,18 @@ export const DEFAULT_PROJECT_ENVS = [ type TProjectServiceFactoryDep = { projectDAL: TProjectDALFactory; - folderDAL: Pick; - projectEnvDAL: Pick; - projectMembershipDAL: Pick; + projectQueue: TProjectQueueFactory; + userDAL: TUserDALFactory; + folderDAL: TSecretFolderDALFactory; + projectEnvDAL: Pick; + identityOrgMembershipDAL: TIdentityOrgDALFactory; + identityProjectDAL: TIdentityProjectDALFactory; + projectKeyDAL: Pick; + projectBotDAL: Pick; + projectMembershipDAL: Pick; secretBlindIndexDAL: Pick; permissionService: TPermissionServiceFactory; + orgService: Pick; licenseService: Pick; }; @@ -38,8 +63,15 @@ export type TProjectServiceFactory = ReturnType; export const projectServiceFactory = ({ projectDAL, + projectQueue, + projectKeyDAL, permissionService, + userDAL, folderDAL, + orgService, + identityProjectDAL, + projectBotDAL, + identityOrgMembershipDAL, secretBlindIndexDAL, projectMembershipDAL, projectEnvDAL, @@ -48,8 +80,13 @@ export const projectServiceFactory = ({ /* * Create workspace. Make user the admin * */ - const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName }: TCreateProjectDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName, slug }: TCreateProjectDTO) => { + const { permission, membership: orgMembership } = await permissionService.getOrgPermission( + actor, + actorId, + orgId, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); const appCfg = getConfig(); @@ -64,20 +101,28 @@ export const projectServiceFactory = ({ }); } - const newProject = projectDAL.transaction(async (tx) => { + const results = await projectDAL.transaction(async (tx) => { + const ghostUser = await orgService.addGhostUser(orgId, tx); + const project = await projectDAL.create( - { name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) }, + { + name: workspaceName, + orgId, + slug: slug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), + version: ProjectVersion.V2 + }, tx ); - // set user as admin member for proeject + // set ghost user as admin of project await projectMembershipDAL.create( { - userId: actorId, + userId: ghostUser.user.id, role: ProjectMembershipRole.Admin, projectId: project.id }, tx ); + // generate the blind index for project await secretBlindIndexDAL.create( { @@ -99,18 +144,165 @@ export const projectServiceFactory = ({ envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })), tx ); - // _id for backward compat - return { ...project, environments: envs, _id: project.id }; + + // 3. Create a random key that we'll use as the project key. + const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ + publicKey: ghostUser.keys.publicKey, + privateKey: ghostUser.keys.plainPrivateKey + }); + + // 4. Save the project key for the ghost user. + await projectKeyDAL.create( + { + projectId: project.id, + receiverId: ghostUser.user.id, + encryptedKey: encryptedProjectKey, + nonce: encryptedProjectKeyIv, + senderId: ghostUser.user.id + }, + tx + ); + + const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey); + + // 5. Create & a bot for the project + await projectBotDAL.create( + { + name: "Infisical Bot (Ghost)", + projectId: project.id, + tag, + iv, + encryptedProjectKey, + encryptedProjectKeyNonce: encryptedProjectKeyIv, + encryptedPrivateKey: ciphertext, + isActive: true, + publicKey: ghostUser.keys.publicKey, + senderId: ghostUser.user.id, + algorithm, + keyEncoding: encoding + }, + tx + ); + + // Find the ghost users latest key + const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx); + + if (!latestKey) { + throw new Error("Latest key not found for user"); + } + + // If the project is being created by a user, add the user to the project as an admin + if (actor === ActorType.USER) { + // Find public key of user + const user = await userDAL.findUserEncKeyByUserId(actorId); + + if (!user) { + throw new Error("User not found"); + } + + const [projectAdmin] = assignWorkspaceKeysToMembers({ + decryptKey: latestKey, + userPrivateKey: ghostUser.keys.plainPrivateKey, + members: [ + { + userPublicKey: user.publicKey, + orgMembershipId: orgMembership.id, + projectMembershipRole: ProjectMembershipRole.Admin + } + ] + }); + + // Create a membership for the user + await projectMembershipDAL.create( + { + projectId: project.id, + userId: user.id, + role: projectAdmin.projectRole + }, + tx + ); + + // Create a project key for the user + await projectKeyDAL.create( + { + encryptedKey: projectAdmin.workspaceEncryptedKey, + nonce: projectAdmin.workspaceEncryptedNonce, + senderId: ghostUser.user.id, + receiverId: user.id, + projectId: project.id + }, + tx + ); + } + + // If the project is being created by an identity, add the identity to the project as an admin + else if (actor === ActorType.IDENTITY) { + // Find identity org membership + const identityOrgMembership = await identityOrgMembershipDAL.findOne( + { + identityId: actorId, + orgId: project.orgId + }, + tx + ); + + // If identity org membership not found, throw error + if (!identityOrgMembership) { + throw new BadRequestError({ + message: `Failed to find identity with id ${actorId}` + }); + } + + // Get the role permission for the identity + const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( + ProjectMembershipRole.Admin, + orgId + ); + + const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPrivilege) + throw new ForbiddenRequestError({ + message: "Failed to add identity to project with more privileged role" + }); + const isCustomRole = Boolean(customRole); + + await identityProjectDAL.create( + { + identityId: actorId, + projectId: project.id, + role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin, + roleId: customRole?.id + }, + tx + ); + } + + return { + ...project, + environments: envs, + _id: project.id + }; }); - return newProject; + return results; }; const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); - const deletedProject = await projectDAL.deleteById(projectId); + const deletedProject = await projectDAL.transaction(async (tx) => { + const project = await projectDAL.deleteById(projectId, tx); + const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(projectId).catch(() => null); + + // Delete the org membership for the ghost user if it's found. + if (projectGhostUser) { + await userDAL.deleteById(projectGhostUser.id, tx); + } + + return project; + }); + return deletedProject; }; @@ -124,6 +316,17 @@ export const projectServiceFactory = ({ return projectDAL.findProjectById(projectId); }; + const updateProject = async ({ projectId, actor, actorId, actorOrgId, update }: TUpdateProjectDTO) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + + const updatedProject = await projectDAL.updateById(projectId, { + name: update.name, + autoCapitalization: update.autoCapitalization + }); + return updatedProject; + }; + const toggleAutoCapitalization = async ({ projectId, actor, @@ -146,12 +349,55 @@ export const projectServiceFactory = ({ return updatedProject; }; + const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => { + const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); + + if (membership?.role !== ProjectMembershipRole.Admin) { + throw new ForbiddenRequestError({ + message: "User must be admin" + }); + } + + const encryptedPrivateKey = infisicalSymmetricEncypt(userPrivateKey); + + await projectQueue.upgradeProject({ + projectId, + startedByUserId: actorId, + encryptedPrivateKey: { + encryptedKey: encryptedPrivateKey.ciphertext, + encryptedKeyIv: encryptedPrivateKey.iv, + encryptedKeyTag: encryptedPrivateKey.tag, + keyEncoding: encryptedPrivateKey.encoding + } + }); + }; + + const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + + const project = await projectDAL.findProjectById(projectId); + + if (!project) { + throw new BadRequestError({ + message: `Project with id ${projectId} not found` + }); + } + + return project.upgradeStatus || null; + }; + return { createProject, deleteProject, getProjects, + updateProject, + getProjectUpgradeStatus, getAProject, toggleAutoCapitalization, - updateName + updateName, + upgradeProject }; }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 2ffea1117..3843450c2 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -1,3 +1,6 @@ +import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas"; +import { TProjectPermission } from "@app/lib/types"; + import { ActorType } from "../auth/auth-type"; export type TCreateProjectDTO = { @@ -6,6 +9,7 @@ export type TCreateProjectDTO = { actorOrgId?: string; orgId: string; workspaceName: string; + slug?: string; }; export type TDeleteProjectDTO = { @@ -21,3 +25,24 @@ export type TGetProjectDTO = { actorOrgId?: string; projectId: string; }; + +export type TUpdateProjectDTO = { + update: { + name?: string; + autoCapitalization?: boolean; + }; +} & TProjectPermission; + +export type TUpgradeProjectDTO = { + userPrivateKey: string; +} & TProjectPermission; + +export type AddUserToWsDTO = { + decryptKey: TProjectKeys & { sender: { publicKey: string } }; + userPrivateKey: string; + members: { + orgMembershipId: string; + projectMembershipRole: ProjectMembershipRole; + userPublicKey: string; + }[]; +}; diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index 1fa55f214..00b33a13c 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -41,13 +41,12 @@ export const fnSecretsFromImports = async ({ environment: importEnv.slug, environmentInfo: importEnv, folderId: importedFolders?.[i]?.id, - secrets: importedFolders?.[i]?.id - ? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string].map((item) => ({ - ...item, - environment: importEnv.slug, - workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. - _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. - })) - : [] + // this will ensure for cases when secrets are empty. Could be due to missing folder for a path or when emtpy secrets inside a given path + secrets: (importedSecsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []).map((item) => ({ + ...item, + environment: importEnv.slug, + workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. + _id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend. + })) })); }; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index a519c7820..1beae9be6 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -4,6 +4,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -21,6 +22,7 @@ type TSecretImportServiceFactoryDep = { secretImportDAL: TSecretImportDALFactory; folderDAL: TSecretFolderDALFactory; secretDAL: Pick; + projectDAL: Pick; projectEnvDAL: TProjectEnvDALFactory; permissionService: Pick; }; @@ -34,6 +36,7 @@ export const secretImportServiceFactory = ({ projectEnvDAL, permissionService, folderDAL, + projectDAL, secretDAL }: TSecretImportServiceFactoryDep) => { const createImport = async ({ @@ -62,6 +65,8 @@ export const secretImportServiceFactory = ({ }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" }); diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index ba65033cb..11cd522ca 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -22,7 +22,11 @@ export const secretDALFactory = (db: TDbClient) => { // the idea is to use postgres specific function // insert with id this will cause a conflict then merge the data - const bulkUpdate = async (data: Array<{ filter: Partial; data: TSecretsUpdate }>, tx?: Knex) => { + const bulkUpdate = async ( + data: Array<{ filter: Partial; data: TSecretsUpdate }>, + + tx?: Knex + ) => { try { const secs = await Promise.all( data.map(async ({ filter, data: updateData }) => { @@ -41,6 +45,35 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const bulkUpdateNoVersionIncrement = async (data: TSecrets[], tx?: Knex) => { + try { + const existingSecrets = await secretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secrets do not exist" }); + } + + if (data.length === 0) return []; + + const updatedSecrets = await (tx || db)(TableName.Secret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedSecrets; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + const deleteMany = async ( data: Array<{ blindIndex: string; type: SecretType }>, folderId: string, @@ -57,6 +90,12 @@ export const secretDALFactory = (db: TDbClient) => { type: el.type, ...(el.type === SecretType.Personal ? { userId } : {}) }); + if (el.type === SecretType.Shared) { + void bd.orWhere({ + secretBlindIndex: el.blindIndex, + type: SecretType.Personal + }); + } }); }) .delete() @@ -139,5 +178,13 @@ export const secretDALFactory = (db: TDbClient) => { } }; - return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes }; + return { + ...secretOrm, + update, + bulkUpdate, + deleteMany, + bulkUpdateNoVersionIncrement, + findByFolderId, + findByBlindIndexes + }; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index ff9dce9a8..96d851a63 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -11,6 +11,7 @@ import { groupBy, pick } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { ActorType } from "../auth/auth-type"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -49,6 +50,7 @@ type TSecretServiceFactoryDep = { secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; folderDAL: Pick; + projectDAL: Pick; secretBlindIndexDAL: TSecretBlindIndexDALFactory; permissionService: Pick; snapshotService: Pick; @@ -68,6 +70,7 @@ export const secretServiceFactory = ({ permissionService, snapshotService, secretQueueService, + projectDAL, projectBotService, secretImportDAL, secretVersionTagDAL @@ -281,6 +284,8 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; @@ -359,6 +364,8 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; @@ -459,6 +466,8 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; @@ -650,6 +659,8 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; @@ -705,6 +716,8 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; @@ -776,6 +789,8 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 7a6695e18..758352ed2 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -1,8 +1,8 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretVersions } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; +import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TSecretVersionDALFactory = ReturnType; @@ -36,6 +36,57 @@ export const secretVersionDALFactory = (db: TDbClient) => { } }; + const bulkUpdate = async ( + data: Array<{ filter: Partial; data: TSecretVersionsUpdate }>, + tx?: Knex + ) => { + try { + const secs = await Promise.all( + data.map(async ({ filter, data: updateData }) => { + const [doc] = await (tx || db)(TableName.SecretVersion) + .where(filter) + .update(updateData) + .increment("version", 1) // TODO: Is this really needed? + .returning("*"); + if (!doc) throw new BadRequestError({ message: "Failed to update document" }); + return doc; + }) + ); + return secs; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + const bulkUpdateNoVersionIncrement = async (data: TSecretVersions[], tx?: Knex) => { + try { + const existingSecretVersions = await secretVersionOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingSecretVersions.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret versions do not exist" }); + } + + if (data.length === 0) return []; + + const updatedSecretVersions = await (tx || db)(TableName.SecretVersion) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedSecretVersions; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => { try { const docs: Array = await (tx || db)(TableName.SecretVersion) @@ -59,5 +110,11 @@ export const secretVersionDALFactory = (db: TDbClient) => { } }; - return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId }; + return { + ...secretVersionOrm, + findLatestVersionMany, + bulkUpdate, + findLatestVersionByFolderId, + bulkUpdateNoVersionIncrement + }; }; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 142993ceb..7ebeaa227 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -25,7 +25,8 @@ export enum SmtpTemplates { OrgInvite = "organizationInvitation.handlebars", ResetPassword = "passwordReset.handlebars", SecretLeakIncident = "secretLeakIncident.handlebars", - WorkspaceInvite = "workspaceInvitation.handlebars" + WorkspaceInvite = "workspaceInvitation.handlebars", + ScimUserProvisioned = "scimUserProvisioned.handlebars" } export enum SmtpHost { diff --git a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars new file mode 100644 index 000000000..b1482aa17 --- /dev/null +++ b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars @@ -0,0 +1,16 @@ + + + + + + + Organization Invitation + + +

Join your organization on Infisical

+

You've been invited to join the Infisical organization โ€” {{organizationName}}

+ Join now +

What is Infisical?

+

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

+ + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/workspaceInvitation.handlebars b/backend/src/services/smtp/templates/workspaceInvitation.handlebars index 60556555c..39a9b74ba 100644 --- a/backend/src/services/smtp/templates/workspaceInvitation.handlebars +++ b/backend/src/services/smtp/templates/workspaceInvitation.handlebars @@ -1,15 +1,15 @@ - - - - + + + Project Invitation - - + +

Join your team on Infisical

-

{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project โ€” {{workspaceName}}

+

You have been invited to a new Infisical project โ€” {{workspaceName}}

Join now

What is Infisical?

-

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

- +

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets + and configs.

+ \ No newline at end of file diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 1144bd414..64dac73f2 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -70,6 +70,7 @@ export const superAdminServiceFactory = ({ lastName, email, superAdmin: true, + isGhost: false, isAccepted: true, authMethods: [AuthMethod.EMAIL] }, @@ -96,7 +97,11 @@ export const superAdminServiceFactory = ({ const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org"; - await orgService.createOrganization(userInfo.user.id, userInfo.user.email, initialOrganizationName); + const organization = await orgService.createOrganization( + userInfo.user.id, + userInfo.user.email, + initialOrganizationName + ); await updateServerCfg({ initialized: true }); const token = await authService.generateUserTokens({ @@ -106,7 +111,7 @@ export const superAdminServiceFactory = ({ organizationId: undefined }); // TODO(akhilmhdh-pg): telemetry service - return { token, user: userInfo }; + return { token, user: userInfo, organization }; }; return { diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 0de490399..2c89b9c4d 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -23,7 +23,7 @@ export const userDALFactory = (db: TDbClient) => { const findUserEncKeyByEmail = async (email: string) => { try { return await db(TableName.Users) - .where({ email }) + .where({ email, isGhost: false }) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .first(); } catch (error) { @@ -47,6 +47,17 @@ export const userDALFactory = (db: TDbClient) => { } }; + const findUserByProjectMembershipId = async (projectMembershipId: string) => { + try { + return await db(TableName.ProjectMembership) + .where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId }) + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .first(); + } catch (error) { + throw new DatabaseError({ error, name: "Find user by project membership id" }); + } + }; + const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => { try { const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*"); @@ -111,6 +122,7 @@ export const userDALFactory = (db: TDbClient) => { findUserEncKeyByEmail, findUserEncKeyByUserId, updateUserEncryptionByUserId, + findUserByProjectMembershipId, upsertUserEncryptionKey, createUserEncryption, findOneUserAction, diff --git a/backend/vitest.e2e.config.ts b/backend/vitest.e2e.config.ts index e8636d405..c660fed14 100644 --- a/backend/vitest.e2e.config.ts +++ b/backend/vitest.e2e.config.ts @@ -4,12 +4,16 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, + env: { + NODE_ENV: "test" + }, environment: "./e2e-test/vitest-environment-knex.ts", include: ["./e2e-test/**/*.spec.ts"], poolOptions: { threads: { singleThread: true, - useAtomics: true + useAtomics: true, + isolate: false } } }, diff --git a/docs/api-reference/endpoints/workspaces/create-workspace.mdx b/docs/api-reference/endpoints/workspaces/create-workspace.mdx new file mode 100644 index 000000000..ec3d09b81 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/create-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Workspace" +openapi: "POST /api/v2/workspace" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/workspaces/delete-workspace.mdx b/docs/api-reference/endpoints/workspaces/delete-workspace.mdx new file mode 100644 index 000000000..ca6cb8914 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/delete-workspace.mdx @@ -0,0 +1,8 @@ +--- +title: "Delete Workspace" +openapi: "DELETE /api/v1/workspace/{workspaceId}" +--- + + + This operation is irreversible. All data associated with the project will be deleted. Please use with caution. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/workspaces/get-workspace.mdx b/docs/api-reference/endpoints/workspaces/get-workspace.mdx new file mode 100644 index 000000000..2d27d9455 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/get-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Workspace" +openapi: "GET /api/v1/workspace/{workspaceId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/workspaces/invite-member-to-workspace.mdx b/docs/api-reference/endpoints/workspaces/invite-member-to-workspace.mdx new file mode 100644 index 000000000..a09c586ca --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/invite-member-to-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Invite Member" +openapi: "POST /api/v2/workspace/{projectId}/memberships" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/workspaces/remove-member-from-workspace.mdx b/docs/api-reference/endpoints/workspaces/remove-member-from-workspace.mdx new file mode 100644 index 000000000..8b781a4e8 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/remove-member-from-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Member" +openapi: "DELETE /api/v2/workspace/{projectId}/memberships" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/workspaces/update-workspace.mdx b/docs/api-reference/endpoints/workspaces/update-workspace.mdx new file mode 100644 index 000000000..81a6ec8ea --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/update-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Workspace" +openapi: "PATCH /api/v1/workspace/{workspaceId}" +--- \ No newline at end of file diff --git a/docs/cli/scanning-overview.mdx b/docs/cli/scanning-overview.mdx index 0163d5dca..748c9d4e1 100644 --- a/docs/cli/scanning-overview.mdx +++ b/docs/cli/scanning-overview.mdx @@ -34,7 +34,7 @@ In addition to scanning for past leaks, this new addition also actively aids in infisical scan git-changes # Display the full secret findings - infisical git-changes --verbose + infisical scan git-changes --verbose ``` Scanning for secrets before you commit your changes is great way to prevent leaks. Infisical makes this easy with the sub command `git-changes`. diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx new file mode 100644 index 000000000..45f95e135 --- /dev/null +++ b/docs/documentation/platform/scim/azure.mdx @@ -0,0 +1,74 @@ +--- +title: "Azure SCIM" +description: "Configure SCIM provisioning with Azure for Infisical" +--- + + + Azure SCIM provisioning is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + +Prerequisites: +- [Configure Azure SAML for Infisical](/documentation/platform/sso/azure) + + + + In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and + press the **Enable SCIM provisioning** toggle to allow Azure to provision/deprovision users for your organization. + + ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) + + Next, press **Manage SCIM Tokens** and then **Create** to generate a SCIM token for Azure. + + ![SCIM create token](/images/platform/scim/scim-create-token.png) + + Next, copy the **SCIM URL** and **New SCIM Token** to use when configuring SCIM in Azure. + + ![SCIM copy token](/images/platform/scim/scim-copy-token.png) + + + In Azure, head to your Enterprise Application > Provisioning > Overview and press **Get started**. + + ![SCIM Azure](/images/platform/scim/azure/scim-azure-get-started.png) + + Next, set the following fields: + + - Provisioning Mode: Select **Automatic**. + - Tenant URL: Input **SCIM URL** from Step 1. + - Secret Token: Input the **New SCIM Token** from Step 1. + + Afterwards, press the **Test Connection** button to check that SCIM is configured properly. + + ![SCIM Azure](/images/platform/scim/azure/scim-azure-config.png) + + After you hit **Save**, select **Provision Microsoft Entra ID Users** under the **Mappings** subsection. + + ![SCIM Azure](/images/platform/scim/azure/scim-azure-select-user-mappings.png) + + Next, adjust the mappings so you have them configured as below: + + ![SCIM Azure](/images/platform/scim/azure/scim-azure-user-mappings.png) + + Finally, head to your Enterprise Application > Provisioning and set the **Provisioning Status** to **On**. + + ![SCIM Azure](/images/platform/scim/azure/scim-azure-provisioning-status.png) + + Alternatively, you can go to **Overview** and press **Start provisioning** to have Azure start provisioning/deprovisioning users to Infisical. + + ![SCIM Azure](/images/platform/scim/azure/scim-azure-start-provisioning.png) + + Now Azure can provision/deprovision users to/from your organization in Infisical. + + + +**FAQ** + + + + Infisical's SCIM implmentation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform. + + For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets. + + \ No newline at end of file diff --git a/docs/documentation/platform/scim/jumpcloud.mdx b/docs/documentation/platform/scim/jumpcloud.mdx new file mode 100644 index 000000000..68bb9b66f --- /dev/null +++ b/docs/documentation/platform/scim/jumpcloud.mdx @@ -0,0 +1,64 @@ +--- +title: "JumpCloud SCIM" +description: "Configure SCIM provisioning with JumpCloud for Infisical" +--- + + + JumpCloud SCIM provisioning is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + +Prerequisites: +- [Configure JumpCloud SAML for Infisical](/documentation/platform/sso/jumpcloud) + + + + In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and + press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users for your organization. + + ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) + + Next, press **Manage SCIM Tokens** and then **Create** to generate a SCIM token for JumpCloud. + + ![SCIM create token](/images/platform/scim/scim-create-token.png) + + Next, copy the **SCIM URL** and **New SCIM Token** to use when configuring SCIM in JumpCloud. + + ![SCIM copy token](/images/platform/scim/scim-copy-token.png) + + + In JumpCloud, head to your Application > Identity Management > Configuration settings and make sure that + **API Type** is set to **SCIM API** and **SCIM Version** is set to **SCIM 2.0**. + + ![SCIM JumpCloud](/images/platform/scim/jumpcloud/scim-jumpcloud-api-type.png) + + Next, set the following SCIM connection fields: + + - Base URL: Input the **SCIM URL** from Step 1. + - Token Key: Input the **New SCIM Token** from Step 1. + - Test User Email: Input a test user email to be used by JumpCloud for testing the SCIM connection. + + Alos, under HTTP Header > Authorization: Bearer, input the **New SCIM Token** from Step 1. + + ![SCIM JumpCloud](/images/platform/scim/jumpcloud/scim-jumpcloud-config.png) + + Next, press **Test Connection** to check that SCIM is configured properly. Finally, press **Activate** + to have JumpCloud start provisioning/deprovisioning users to Infisical. + + ![SCIM JumpCloud](/images/platform/scim/jumpcloud/scim-jumpcloud-test-connection.png) + + Now JumpCloud can provision/deprovision users to/from your organization in Infisical. + + + +**FAQ** + + + + Infisical's SCIM implmentation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform. + + For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets. + + \ No newline at end of file diff --git a/docs/documentation/platform/scim/okta.mdx b/docs/documentation/platform/scim/okta.mdx new file mode 100644 index 000000000..4baa19815 --- /dev/null +++ b/docs/documentation/platform/scim/okta.mdx @@ -0,0 +1,70 @@ +--- +title: "Okta SCIM" +description: "Configure SCIM provisioning with Okta for Infisical" +--- + + + Okta SCIM provisioning is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + +Prerequisites: +- [Configure Okta SAML for Infisical](/documentation/platform/sso/okta) + + + + In Infisical, head to your Organization Settings > Authentication > SCIM Configuration and + press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users for your organization. + + ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) + + Next, press **Manage SCIM Tokens** and then **Create** to generate a SCIM token for Okta. + + ![SCIM create token](/images/platform/scim/scim-create-token.png) + + Next, copy the **SCIM URL** and **New SCIM Token** to use when configuring SCIM in Okta. + + ![SCIM copy token](/images/platform/scim/scim-copy-token.png) + + + In Okta, head to your Application > General > App Settings. Next, select **Edit** and check the box + labled **Enable SCIM provisioning**. + + ![SCIM Okta](/images/platform/scim/okta/scim-okta-enable-provisioning.png) + + Next, head to Provisioning > Integration and set the following SCIM connection fields: + + - SCIM connector base URL: Input the **SCIM URL** from Step 1. + - Unique identifier field for users: Input `email`. + - Supported provisioning actions: Select **Push New Users** and **Push Profile Updates**. + - Authentication Mode: `HTTP Header`. + + ![SCIM Okta](/images/platform/scim/okta/scim-okta-config.png) + + Under HTTP Header > Authorization: Bearer, input the **New SCIM Token** from Step 1. + + ![SCIM Okta](/images/platform/scim/okta/scim-okta-auth.png) + + Next, press **Test Connector Configuration** to check that SCIM is configured properly. + + ![SCIM Okta](/images/platform/scim/okta/scim-okta-test.png) + + Next, head to Provisioning > To App and check the boxes labeled **Enable** for **Create Users**, **Update User Attributes**, and **Deactivate Users**. + + ![SCIM Okta](/images/platform/scim/okta/scim-okta-app-settings.png) + + Now Okta can provision/deprovision users to/from your organization in Infisical. + + + +**FAQ** + + + + Infisical's SCIM implmentation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform. + + For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets. + + \ No newline at end of file diff --git a/docs/documentation/platform/scim/overview.mdx b/docs/documentation/platform/scim/overview.mdx new file mode 100644 index 000000000..deec8b630 --- /dev/null +++ b/docs/documentation/platform/scim/overview.mdx @@ -0,0 +1,32 @@ +--- +title: "SCIM Overview" +description: "Provision users for Infisical via SCIM" +--- + + + SCIM provisioning is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + +You can configure your organization in Infisical to have members be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc. + +- Provisioning: The SCIM provider pushes user information to Infisical. If the user exists in Infisical, Infisical sends an email invitation to add them to the relevant organization in Infisical; if not, Infisical initializes a new user and sends them an email invitation to finish setting up their account in the organization. +- Deprovisioning: The SCIM provider instructs Infisical to remove user(s) from an organization in Infisical. + +SCIM providers: + +- [Okta SCIM](/documentation/platform/scim/okta) +- [Azure SCIM](/documentation/platform/scim/azure) +- [JumpCloud SCIM](/documentation/platform/scim/jumpcloud) + +**FAQ** + + + + Infisical's SCIM implementation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform. + + For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets. + + \ No newline at end of file diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 0f644834a..25303fed6 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -12,7 +12,7 @@ description: "Configure Azure SAML for Infisical SSO" - In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application. @@ -101,6 +101,11 @@ description: "Configure Azure SAML for Infisical SSO" To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Azure user with Infisical; Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. + + + We recommend ensuring that your account is provisioned the application in Azure + prior to enforcing SAML SSO to prevent any unintended issues. + diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index 2273a8852..d25ec0b6d 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -12,7 +12,7 @@ description: "Configure JumpCloud SAML for Infisical SSO" - In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. Next, copy the **ACS URL** and **SP Entity ID** to use when configuring the JumpCloud SAML application. @@ -81,6 +81,11 @@ description: "Configure JumpCloud SAML for Infisical SSO" To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one JumpCloud user with Infisical; Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. + + + We recommend ensuring that your account is provisioned the application in JumpCloud + prior to enforcing SAML SSO to prevent any unintended issues. + diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 576bd769a..bac8efb68 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -12,7 +12,7 @@ description: "Configure Okta SAML 2.0 for Infisical SSO" - In Infisical, head over to your organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. Next, copy the **Single sign-on URL** and **Audience URI (SP Entity ID)** to use when configuring the Okta SAML 2.0 application. ![Okta SAML initial configuration](../../../images/sso/okta/init-config.png) @@ -84,6 +84,11 @@ description: "Configure Okta SAML 2.0 for Infisical SSO" To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Okta user with Infisical; Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. + + + We recommend ensuring that your account is provisioned the application in Okta + prior to enforcing SAML SSO to prevent any unintended issues. + diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 8f4b3bb0f..cd2f8ff31 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -3,13 +3,13 @@ title: "SSO Overview" description: "Log in to Infisical via SSO protocols" --- - + Infisical offers Google SSO and GitHub SSO for free across both Infisical Cloud and Infisical Self-hosted. Infisical also offers SAML SSO authentication but as paid features that can be unlocked on Infisical Cloud's **Pro** tier or via enterprise license on self-hosted instances of Infisical. On this front, we support industry-leading providers including - Okta, Azure AD, and JumpCloud; with any questions, please reach out to [sales@infisical.com](mailto:sales@infisical.com). - + Okta, Azure AD, and JumpCloud; with any questions, please reach out to team@infisical.com. + You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0). diff --git a/docs/images/platform/scim/azure/scim-azure-config.png b/docs/images/platform/scim/azure/scim-azure-config.png new file mode 100644 index 000000000..5255c3a8d Binary files /dev/null and b/docs/images/platform/scim/azure/scim-azure-config.png differ diff --git a/docs/images/platform/scim/azure/scim-azure-get-started.png b/docs/images/platform/scim/azure/scim-azure-get-started.png new file mode 100644 index 000000000..c97574673 Binary files /dev/null and b/docs/images/platform/scim/azure/scim-azure-get-started.png differ diff --git a/docs/images/platform/scim/azure/scim-azure-provisioning-status.png b/docs/images/platform/scim/azure/scim-azure-provisioning-status.png new file mode 100644 index 000000000..d457a1170 Binary files /dev/null and b/docs/images/platform/scim/azure/scim-azure-provisioning-status.png differ diff --git a/docs/images/platform/scim/azure/scim-azure-select-user-mappings.png b/docs/images/platform/scim/azure/scim-azure-select-user-mappings.png new file mode 100644 index 000000000..2654f86fe Binary files /dev/null and b/docs/images/platform/scim/azure/scim-azure-select-user-mappings.png differ diff --git a/docs/images/platform/scim/azure/scim-azure-start-provisioning.png b/docs/images/platform/scim/azure/scim-azure-start-provisioning.png new file mode 100644 index 000000000..949474a49 Binary files /dev/null and b/docs/images/platform/scim/azure/scim-azure-start-provisioning.png differ diff --git a/docs/images/platform/scim/azure/scim-azure-user-mappings.png b/docs/images/platform/scim/azure/scim-azure-user-mappings.png new file mode 100644 index 000000000..b96ab6cf7 Binary files /dev/null and b/docs/images/platform/scim/azure/scim-azure-user-mappings.png differ diff --git a/docs/images/platform/scim/jumpcloud/scim-jumpcloud-api-type.png b/docs/images/platform/scim/jumpcloud/scim-jumpcloud-api-type.png new file mode 100644 index 000000000..b10fd099d Binary files /dev/null and b/docs/images/platform/scim/jumpcloud/scim-jumpcloud-api-type.png differ diff --git a/docs/images/platform/scim/jumpcloud/scim-jumpcloud-config.png b/docs/images/platform/scim/jumpcloud/scim-jumpcloud-config.png new file mode 100644 index 000000000..1c42729a7 Binary files /dev/null and b/docs/images/platform/scim/jumpcloud/scim-jumpcloud-config.png differ diff --git a/docs/images/platform/scim/jumpcloud/scim-jumpcloud-test-connection.png b/docs/images/platform/scim/jumpcloud/scim-jumpcloud-test-connection.png new file mode 100644 index 000000000..e1980fdfb Binary files /dev/null and b/docs/images/platform/scim/jumpcloud/scim-jumpcloud-test-connection.png differ diff --git a/docs/images/platform/scim/okta/scim-okta-app-settings.png b/docs/images/platform/scim/okta/scim-okta-app-settings.png new file mode 100644 index 000000000..a3ea836ec Binary files /dev/null and b/docs/images/platform/scim/okta/scim-okta-app-settings.png differ diff --git a/docs/images/platform/scim/okta/scim-okta-auth.png b/docs/images/platform/scim/okta/scim-okta-auth.png new file mode 100644 index 000000000..97ad34567 Binary files /dev/null and b/docs/images/platform/scim/okta/scim-okta-auth.png differ diff --git a/docs/images/platform/scim/okta/scim-okta-config.png b/docs/images/platform/scim/okta/scim-okta-config.png new file mode 100644 index 000000000..b20ceddca Binary files /dev/null and b/docs/images/platform/scim/okta/scim-okta-config.png differ diff --git a/docs/images/platform/scim/okta/scim-okta-enable-provisioning.png b/docs/images/platform/scim/okta/scim-okta-enable-provisioning.png new file mode 100644 index 000000000..d5688182e Binary files /dev/null and b/docs/images/platform/scim/okta/scim-okta-enable-provisioning.png differ diff --git a/docs/images/platform/scim/okta/scim-okta-test.png b/docs/images/platform/scim/okta/scim-okta-test.png new file mode 100644 index 000000000..f1c2e9221 Binary files /dev/null and b/docs/images/platform/scim/okta/scim-okta-test.png differ diff --git a/docs/images/platform/scim/scim-copy-token.png b/docs/images/platform/scim/scim-copy-token.png new file mode 100644 index 000000000..d3a4586c2 Binary files /dev/null and b/docs/images/platform/scim/scim-copy-token.png differ diff --git a/docs/images/platform/scim/scim-create-token.png b/docs/images/platform/scim/scim-create-token.png new file mode 100644 index 000000000..9fe4eac3e Binary files /dev/null and b/docs/images/platform/scim/scim-create-token.png differ diff --git a/docs/images/platform/scim/scim-enable-provisioning.png b/docs/images/platform/scim/scim-enable-provisioning.png new file mode 100644 index 000000000..a4385244f Binary files /dev/null and b/docs/images/platform/scim/scim-enable-provisioning.png differ diff --git a/docs/integrations/platforms/ansible.mdx b/docs/integrations/platforms/ansible.mdx index efb9e4f63..2d524d55c 100644 --- a/docs/integrations/platforms/ansible.mdx +++ b/docs/integrations/platforms/ansible.mdx @@ -5,6 +5,19 @@ description: "How to use Infisical for secret management in Ansible" The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/). - - Have any questions? Join Infisical's [community Slack](https://infisical.com/slack) for quick support. - +## Troubleshoot + + + If you get this Python error when you running the lookup plugin:- + + ``` + objc[72832]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug. + Fatal Python error: Aborted + ``` + + You will need to add this to your shell environment or ansible wrapper script:- + + ``` + export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES + ``` + diff --git a/docs/integrations/platforms/docker.mdx b/docs/integrations/platforms/docker.mdx index e9682b785..8fbf288ad 100644 --- a/docs/integrations/platforms/docker.mdx +++ b/docs/integrations/platforms/docker.mdx @@ -51,7 +51,7 @@ CMD ["infisical", "run", "--", "npm", "run", "start"] CMD ["infisical", "run", "--command", "npm run start && ..."] ``` -## Generate an service token +## Generate a service token Head to your project settings in the Infisical dashboard to generate an [service token](/documentation/platform/token). This service token will allow you to authenticate and fetch secrets from Infisical. diff --git a/docs/mint.json b/docs/mint.json index b67493aeb..ff0ceb165 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -148,16 +148,27 @@ "documentation/platform/sso/azure", "documentation/platform/sso/jumpcloud" ] + }, + { + "group": "SCIM", + "pages": [ + "documentation/platform/scim/overview", + "documentation/platform/scim/okta", + "documentation/platform/scim/azure", + "documentation/platform/scim/jumpcloud" + ] } ] }, { "group": "Self-host Infisical", "pages": [ + "self-hosting/overview", + "self-hosting/configuration/requirements", + "self-hosting/configuration/schema-migrations", { - "group": "Deployment options", + "group": "Installation methods", "pages": [ - "self-hosting/overview", "self-hosting/deployment-options/standalone-infisical", "self-hosting/deployment-options/docker-compose", "self-hosting/deployment-options/kubernetes-helm", @@ -172,9 +183,6 @@ ] }, "self-hosting/configuration/envars", - "self-hosting/configuration/email", - "self-hosting/configuration/redis", - "self-hosting/configuration/sso", "self-hosting/faq" ] }, @@ -377,9 +385,14 @@ { "group": "Projects", "pages": [ + "api-reference/endpoints/workspaces/create-workspace", + "api-reference/endpoints/workspaces/delete-workspace", + "api-reference/endpoints/workspaces/get-workspace", + "api-reference/endpoints/workspaces/update-workspace", + "api-reference/endpoints/workspaces/invite-member-to-workspace", + "api-reference/endpoints/workspaces/remove-member-from-workspace", "api-reference/endpoints/workspaces/memberships", "api-reference/endpoints/workspaces/update-membership", - "api-reference/endpoints/workspaces/delete-membership", "api-reference/endpoints/workspaces/list-identity-memberships", "api-reference/endpoints/workspaces/update-identity-membership", "api-reference/endpoints/workspaces/delete-identity-membership", diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx deleted file mode 100644 index c26c20648..000000000 --- a/docs/self-hosting/configuration/email.mdx +++ /dev/null @@ -1,242 +0,0 @@ ---- -title: "Configure email service" -description: "How to configure your email when self-hosting Infisical." ---- - -By default, the core functions of Infisical work without any email service configuration. Without email service, basic sign up/login and secret operations will function without any issue. -However, the following functionality will be disabled. - -- Multi-factor authentication -- Sending invite links via email for projects to teammates -- Sending alerts such as suspicious login attempts - -## Configuration - -If you choose to setup email service, you need to configure the following SMTP [environment variables](https://infisical.com/docs/self-hosting/configuration/envars): - -- `SMTP_HOST`: Hostname to connect to for establishing SMTP connections. -- `SMTP_USERNAME`: Credential to connect to host (e.g. team@infisical.com) -- `SMTP_PASSWORD`: Credential to connect to host. -- `SMTP_PORT`: Port to connect to for establishing SMTP connections. -- `SMTP_SECURE`: If `true`, the connection will use TLS when connecting to server with special configs for SendGrid and Mailgun. If `false` (the default) then TLS is used if server supports the STARTTLS extension. -- `SMTP_FROM_ADDRESS`: Email address to be used for sending emails (e.g. team@infisical.com). -- `SMTP_FROM_NAME`: Name label to be used in `From` field (e.g. Team). - -Below you will find details on how to configure common email providers: - - - -1. Create an account on [Resend](https://resend.com). -2. Add a [Domain](https://resend.com/domains). - -![adding resend domain](../../images/self-hosting/configuration/email/email-resend-create-domain.png) - -3. Create an [API Key](https://resend.com/api-keys). - -![creating resend api key](../../images/self-hosting/configuration/email/email-resend-create-key.png) - -4. Go to the [SMTP page](https://resend.com/settings/smtp) and copy the values. - -![go to resend smtp settings](../../images/self-hosting/configuration/email/email-resend-smtp-settings.png) - -5. With the API Key, you can now set your SMTP environment variables variables: - -``` -SMTP_HOST=smtp.resend.com -SMTP_USERNAME=resend -SMTP_PASSWORD=YOUR_API_KEY -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails -SMTP_FROM_NAME=Infisical -``` - - Remember that you will need to restart Infisical for this to work properly. - - - - - -1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails. -2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys) -3. Set a name for your API Key, we recommend using "Infisical," and select the "Restricted Key" option. You will need to enable the "Mail Send" permission as shown below: - -![creating sendgrid api key](../../images/self-hosting/configuration/email/email-sendgrid-create-key.png) - -![setting sendgrid api key restriction](../../images/self-hosting/configuration/email/email-sendgrid-restrictions.png) - -4. With the API Key, you can now set your SMTP environment variables: - -``` -SMTP_HOST=smtp.sendgrid.net -SMTP_USERNAME=apikey -SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails -SMTP_FROM_NAME=Infisical -``` - - - Remember that you will need to restart Infisical for this to work properly. - - - - - -1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails. -2. Obtain your Mailgun credentials in Sending > Overview > SMTP - -![obtain mailhog api key estriction](../../images/self-hosting/configuration/email/email-mailhog-credentials.png) - -3. With your Mailgun credentials, you can now set up your SMTP environment variables: - -``` -SMTP_HOST=smtp.mailgun.org # obtained from credentials page -SMTP_USERNAME=postmaster@example.mailgun.org # obtained from credentials page -SMTP_PASSWORD=password # obtained from credentials page -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails -SMTP_FROM_NAME=Infisical -``` - - - - - -1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console. -2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials - -![opening AWS SES console](../../images/self-hosting/configuration/email/email-aws-ses-console.png) - -![creating AWS IAM SES user](../../images/self-hosting/configuration/email/email-aws-ses-user.png) - -3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables: - -``` -SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings -SMTP_USERNAME=xxx # your SMTP username -SMTP_PASSWORD=xxx # your SMTP password -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails -SMTP_FROM_NAME=Infisical -``` - - - Remember that you will need to restart Infisical for this to work properly. - - - - - -1. Create an account and configure [SocketLabs](https://www.socketlabs.com/) to send emails. -2. From the dashboard, navigate to SMTP Credentials > SMTP & APIs > SMTP Credentials to obtain your SocketLabs SMTP credentials. - -![opening SocketLabs dashboard](../../images/self-hosting/configuration/email/email-socketlabs-dashboard.png) - -![obtaining SocketLabs credentials](../../images/self-hosting/configuration/email/email-socketlabs-credentials.png) - -3. With your SocketLabs SMTP credentials, you can now set up your SMTP environment variables: - -``` -SMTP_HOST=smtp.socketlabs.com -SMTP_USERNAME=username # obtained from your credentials -SMTP_PASSWORD=password # obtained from your credentials -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails -SMTP_FROM_NAME=Infisical -``` - - - The `SMTP_FROM_ADDRESS` environment variable should be an email for an - authenticated domain under Configuration > Domain Management in SocketLabs. - For example, if you're using SocketLabs in sandbox mode, then you may use an - email like `team@sandbox.socketlabs.dev`. - - -![SocketLabs domain management](../../images/self-hosting/configuration/email/email-socketlabs-domains.png) - - - Remember that you will need to restart Infisical for this to work properly. - - - - - -Create an account and enable "less secure app access" in Gmail Account Settings > Security. This will allow -applications like Infisical to authenticate with Gmail via your username and password. - -![Gmail secure app access](../../images/self-hosting/configuration/email/email-gmail-app-access.png) - -With your Gmail username and password, you can set your SMTP environment variables: - -``` -SMTP_HOST=smtp.gmail.com -SMTP_USERNAME=hey@gmail.com # your email -SMTP_PASSWORD=password # your password -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=hey@gmail.com -SMTP_FROM_NAME=Infisical -``` - - - As per the [notice](https://support.google.com/accounts/answer/6010255?hl=en) by Google, you should note that using Gmail credentials for SMTP configuration - will only work for Google Workspace or Google Cloud Identity customers as of May 30, 2022. - -Put differently, the SMTP configuration is only possible with business (not personal) Gmail credentials. - - - - - - - -1. Create an account and configure [Office365](https://www.office.com/) to send emails. - -2. With your login credentials, you can now set up your SMTP environment variables: - -``` -SMTP_HOST=smtp.office365.com -SMTP_USERNAME=username@yourdomain.com # your username -SMTP_PASSWORD=password # your password -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=username@yourdomain.com -SMTP_FROM_NAME=Infisical -``` - - - - - -1. Create an account and configure [Zoho Mail](https://www.zoho.com/mail/) to send emails. - -2. With your email credentials, you can now set up your SMTP environment variables: - -``` -SMTP_HOST=smtp.zoho.com -SMTP_USERNAME=username # your email -SMTP_PASSWORD=password # your password -SMTP_PORT=587 -SMTP_SECURE=true -SMTP_FROM_ADDRESS=hey@example.com # your personal Zoho email or domain-based email linked to Zoho Mail -SMTP_FROM_NAME=Infisical -``` - - - You can use either your personal Zoho email address like `you@zohomail.com` or - a domain-based email address like `you@yourdomain.com`. If using a - domain-based email address, then please make sure that you've configured and - verified it with Zoho Mail. - - - - Remember that you will need to restart Infisical for this to work properly. - - - - diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 3ea1762e2..333ddba5c 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -1,188 +1,426 @@ --- -title: "All environment variables" -description: "Configure your environment variables when self-hosting Infisical." +title: "Configurations" +description: "Configure environment variables for self-hosted Infisical" --- -## Environment variables -Depending on your chosen self hosted deployment method, you may need to configured at least the required environment variable listed below. -Other environment variables are listed below to increase the functionality of your self hosted instance based on your use case. +Infisical accepts all configurations via environment variables. For a basic self-hosted instance, at least `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI` and `REDIS_URL` must be defined. +However, you can configure additional settings to activate more features as needed. - - - - Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - +## General platform +Used to configure platform-specific security and operational settings - - Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32` - - - - Mongo connection string. *TLS based connection string is not yet supported - - - - Redis connection string - - - - When email service is not configured, Infisical will have limited functionality - - - Hostname to connect to for establishing SMTP connections - - - - Credential to connect to host (e.g. team@infisical.com) - - - - Credential to connect to host - - - - Port to connect to for establishing SMTP connections - - - - If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported - - - - Email address to be used for sending emails - - - - Name label to be used in From field (e.g. Team) - - - - - To sync secret to third party services, provide value for the related services - - - OAuth2 client ID for Heroku integration - - - - OAuth2 client secret for Heroku integration - - - - OAuth2 client ID for Vercel integration - - - - OAuth2 client secret for Vercel integration - - - - OAuth2 client ID for Netlify integration - - - - OAuth2 client secret for Netlify integration - - - - OAuth2 client ID for GitHub integration - - - - OAuth2 client secret for GitHub integration - - - - OAuth2 slug for Vercel integration - - - - OAuth2 client ID for BitBucket integration - - - - OAuth2 client secret for BitBucket integration - - - - - To integrate with external auth providers, provide value for the related keys - - OAuth2 client ID for Google login - - - OAuth2 client secret for Google login - - - OAuth2 client ID for GitHub login - - - OAuth2 client secret for GitHub login - - - OAuth2 client ID for GitLab login - - - OAuth2 client secret for GitLab login - - - URL of your self-hosted instance of GitLab where the OAuth application is registered - - - - #### JWT - - JWT token lifetime expressed in seconds or a string describing a time span - - - - JWT token lifetime expressed in seconds or a string describing a time span - - - - JWT token lifetime expressed in seconds or a string describing a time span - - - - JWT token lifetime expressed in seconds or a string describing a time span - - - - JWT token lifetime expressed in seconds or a string describing a time span - - -#### Logging - -Infisical uses Sentry to report error logs - - - The minimum log level for application logging; can be one of `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. + + Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16` - - -#### Settings - -{" "} - - - Only allow users who are invited to sign up + + Must be a random 32 byte base64 string. Can be generated with `openssl rand -base64 32` - - Site URL - should be an absolute URL including the protocol (e.g. https://app.infisical.com) + + Must be an absolute URL including the protocol (e.g. https://app.infisical.com). + + +## Data Layer +The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks + + + Postgres database connection string. + + + + Configure the SSL certificate for securing a Postgres connection by first encoding it in base64. + Use the command below to encode your certificate: + `echo "" | base64` + + + + Redis connection string. + + + + +## Email service +Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features. + + + + Hostname to connect to for establishing SMTP connections - - - + + + Credential to connect to host (e.g. team@infisical.com) + + + + Credential to connect to host + + + + Port to connect to for establishing SMTP connections + + + + If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported + + + + Email address to be used for sending emails + + + + Name label to be used in From field (e.g. Team) + + + + + + 1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails. + 2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys) + 3. Set a name for your API Key, we recommend using "Infisical," and select the "Restricted Key" option. You will need to enable the "Mail Send" permission as shown below: + + ![creating sendgrid api key](../../images/self-hosting/configuration/email/email-sendgrid-create-key.png) + + ![setting sendgrid api key restriction](../../images/self-hosting/configuration/email/email-sendgrid-restrictions.png) + + 4. With the API Key, you can now set your SMTP environment variables: + + ``` + SMTP_HOST=smtp.sendgrid.net + SMTP_USERNAME=apikey + SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails + SMTP_FROM_NAME=Infisical + ``` + + + Remember that you will need to restart Infisical for this to work properly. + + + + + 1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails. + 2. Obtain your Mailgun credentials in Sending > Overview > SMTP + + ![obtain mailhog api key estriction](../../images/self-hosting/configuration/email/email-mailhog-credentials.png) + + 3. With your Mailgun credentials, you can now set up your SMTP environment variables: + + ``` + SMTP_HOST=smtp.mailgun.org # obtained from credentials page + SMTP_USERNAME=postmaster@example.mailgun.org # obtained from credentials page + SMTP_PASSWORD=password # obtained from credentials page + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails + SMTP_FROM_NAME=Infisical + ``` + + + + 1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console. + 2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials + + ![opening AWS SES console](../../images/self-hosting/configuration/email/email-aws-ses-console.png) + + ![creating AWS IAM SES user](../../images/self-hosting/configuration/email/email-aws-ses-user.png) + + 3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables: + + ``` + SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings + SMTP_USERNAME=xxx # your SMTP username + SMTP_PASSWORD=xxx # your SMTP password + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails + SMTP_FROM_NAME=Infisical + ``` + + + Remember that you will need to restart Infisical for this to work properly. + + + + + 1. Create an account and configure [SocketLabs](https://www.socketlabs.com/) to send emails. + 2. From the dashboard, navigate to SMTP Credentials > SMTP & APIs > SMTP Credentials to obtain your SocketLabs SMTP credentials. + + ![opening SocketLabs dashboard](../../images/self-hosting/configuration/email/email-socketlabs-dashboard.png) + + ![obtaining SocketLabs credentials](../../images/self-hosting/configuration/email/email-socketlabs-credentials.png) + + 3. With your SocketLabs SMTP credentials, you can now set up your SMTP environment variables: + + ``` + SMTP_HOST=smtp.socketlabs.com + SMTP_USERNAME=username # obtained from your credentials + SMTP_PASSWORD=password # obtained from your credentials + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails + SMTP_FROM_NAME=Infisical + ``` + + + The `SMTP_FROM_ADDRESS` environment variable should be an email for an + authenticated domain under Configuration > Domain Management in SocketLabs. + For example, if you're using SocketLabs in sandbox mode, then you may use an + email like `team@sandbox.socketlabs.dev`. + + + ![SocketLabs domain management](../../images/self-hosting/configuration/email/email-socketlabs-domains.png) + + + Remember that you will need to restart Infisical for this to work properly. + + + + + 1. Create an account on [Resend](https://resend.com). + 2. Add a [Domain](https://resend.com/domains). + + ![adding resend domain](../../images/self-hosting/configuration/email/email-resend-create-domain.png) + + 3. Create an [API Key](https://resend.com/api-keys). + + ![creating resend api key](../../images/self-hosting/configuration/email/email-resend-create-key.png) + + 4. Go to the [SMTP page](https://resend.com/settings/smtp) and copy the values. + + ![go to resend smtp settings](../../images/self-hosting/configuration/email/email-resend-smtp-settings.png) + + 5. With the API Key, you can now set your SMTP environment variables variables: + + ``` + SMTP_HOST=smtp.resend.com + SMTP_USERNAME=resend + SMTP_PASSWORD=YOUR_API_KEY + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails + SMTP_FROM_NAME=Infisical + ``` + + Remember that you will need to restart Infisical for this to work properly. + + + + + Create an account and enable "less secure app access" in Gmail Account Settings > Security. This will allow + applications like Infisical to authenticate with Gmail via your username and password. + + ![Gmail secure app access](../../images/self-hosting/configuration/email/email-gmail-app-access.png) + + With your Gmail username and password, you can set your SMTP environment variables: + + ``` + SMTP_HOST=smtp.gmail.com + SMTP_USERNAME=hey@gmail.com # your email + SMTP_PASSWORD=password # your password + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=hey@gmail.com + SMTP_FROM_NAME=Infisical + ``` + + + As per the [notice](https://support.google.com/accounts/answer/6010255?hl=en) by Google, you should note that using Gmail credentials for SMTP configuration + will only work for Google Workspace or Google Cloud Identity customers as of May 30, 2022. + + Put differently, the SMTP configuration is only possible with business (not personal) Gmail credentials. + + + + + + 1. Create an account and configure [Office365](https://www.office.com/) to send emails. + + 2. With your login credentials, you can now set up your SMTP environment variables: + + ``` + SMTP_HOST=smtp.office365.com + SMTP_USERNAME=username@yourdomain.com # your username + SMTP_PASSWORD=password # your password + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=username@yourdomain.com + SMTP_FROM_NAME=Infisical + ``` + + + + 1. Create an account and configure [Zoho Mail](https://www.zoho.com/mail/) to send emails. + + 2. With your email credentials, you can now set up your SMTP environment variables: + + ``` + SMTP_HOST=smtp.zoho.com + SMTP_USERNAME=username # your email + SMTP_PASSWORD=password # your password + SMTP_PORT=587 + SMTP_SECURE=true + SMTP_FROM_ADDRESS=hey@example.com # your personal Zoho email or domain-based email linked to Zoho Mail + SMTP_FROM_NAME=Infisical + ``` + + + You can use either your personal Zoho email address like `you@zohomail.com` or + a domain-based email address like `you@yourdomain.com`. If using a + domain-based email address, then please make sure that you've configured and + verified it with Zoho Mail. + + + + Remember that you will need to restart Infisical for this to work properly. + + + + + + + +## SSO based login +By default, users can only login via email/password based login method. +To login into Infisical with OAuth providers such as Google, configure the associated variables. + + + Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google) + + + OAuth2 client ID for Google login + + + OAuth2 client secret for Google login + + + + + Follow detailed guide to configure [GitHub SSO](/documentation/platform/sso/github) + + + OAuth2 client ID for GitHub login + + + OAuth2 client secret for GitHub login + + + + + Follow detailed guide to configure [GitLab SSO](/documentation/platform/sso/gitlab) + + + OAuth2 client ID for GitLab login + + + OAuth2 client secret for GitLab login + + + URL of your self-hosted instance of GitLab where the OAuth application is registered + + + + + Requires enterprise license. Please contact team@infisical.com to get more information. + + + + Requires enterprise license. Please contact team@infisical.com to get more information. + + + + Requires enterprise license. Please contact team@infisical.com to get more information. + + + + + + +## Native secret integrations +To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box. + + + + OAuth2 client ID for Heroku integration + + + OAuth2 client secret for Heroku integration + + + + + + OAuth2 client ID for Vercel integration + + + + OAuth2 client secret for Vercel integration + + + + OAuth2 slug for Vercel integration + + + + + + OAuth2 client ID for Netlify integration + + + + OAuth2 client secret for Netlify integration + + + + + + OAuth2 client ID for GitHub integration + + + + OAuth2 client secret for GitHub integration + + + + + + OAuth2 client ID for BitBucket integration + + + + OAuth2 client secret for BitBucket integration + + + + + + OAuth2 client id for GCP secrets manager integration + + + + OAuth2 client secret for GCP secrets manager integration + + + + + + OAuth2 client id for Azure integration + + + + OAuth2 client secret for Azure integration + + + + + + OAuth2 client id for Gitlab integration + + + + OAuth2 client secret for Gitlab integration + + diff --git a/docs/self-hosting/configuration/redis.mdx b/docs/self-hosting/configuration/redis.mdx deleted file mode 100644 index 6013cab87..000000000 --- a/docs/self-hosting/configuration/redis.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: "Configure Redis" -description: "Learn to configure Redis with your self hosted Infisical" ---- - -## Why Redis? -As the features and use case of Infisical have grown, the need for a fast and reliable in-memory data storage has become clear. -By adding Redis to Infisical, we can now support more complex workflows such as queuing system to run long running asynchronous tasks, cron jobs, and access reliable cache to speed up frequently used resources. - - - Starting with Infisical version v0.31.0, Redis will be required to fully use Infisical - - -### Adding Redis to your self hosted instance of Infisical -To add Redis to your self hosted instance, follow the instructions for the deployment method you used. - - - - ### In cluster Redis - By default, new versions of the Infisical Helm chart already comes with an in-cluster Redis instance. To deploy a in-cluster Redis instance along with your Infisical instance, update your Infisical chart then redeploy/upgrade your release. - This will spin up a Redis instance and automatically configure it with your Infisical backend. - - 1. Update Infisical Helm chart - ```bash - helm repo update - ``` - - 2. Upgrade Infisical release - ```bash - helm upgrade infisical-helm-charts/infisical --values - ``` - ### External Redis - If you want to use an external Redis instance, please add a Redis connection URL under the backend environments variables and then upgrade/redeploy your Infisical instance. - - 1. Update your helm values file - ```yaml your-values.yaml - backendEnvironmentVariables: - REDIS_URL= - ``` - - 2. Upgrade Infisical release - ```bash - helm upgrade infisical-helm-charts/infisical --values - ``` - - - ### Internal Redis service - By default, new versions of the docker compose file already comes with a Redis service. To use the pre-configured Redis service, please update your docker compose file to the latest version. - - 1. Download the new docker compose file - ``` - wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml - ``` - 2. Add Redis environment variable to your .env file - ```.env .env - REDIS_URL=redis://redis:6379 - ``` - - 3. Restart your docker compose services - - - This standalone version of Infisical does not have an internal Redis service. To configure Redis with your Infisical instance, you must connect to a external Redis service by setting the connection string as an environment variable. - - Example: - - ```bash - docker run -p 80:80 \ - -e ENCRYPTION_KEY=f40c9178624764ad85a6830b37ce239a \ - -e JWT_SIGNUP_SECRET=38ea90fb7998b92176080f457d890392 \ - -e JWT_REFRESH_SECRET=7764c7bbf3928ad501591a3e005eb364 \ - -e JWT_AUTH_SECRET=5239fea3a4720c0e524f814a540e14a2 \ - -e JWT_SERVICE_SECRET=8509fb8b90c9b53e9e61d1e35826dcb5 \ - -e REDIS_URL=<> \ - -e MONGO_URL="<>" \ - infisical/infisical:latest - ``` - - Redis environment variable name: `REDIS_URL` - - - -## Support -If you have questions or need support, please join our [slack channel](https://infisical-users.slack.com) and one of our teammates will be happy to guide you. \ No newline at end of file diff --git a/docs/self-hosting/configuration/requirements.mdx b/docs/self-hosting/configuration/requirements.mdx new file mode 100644 index 000000000..45b1b92ff --- /dev/null +++ b/docs/self-hosting/configuration/requirements.mdx @@ -0,0 +1,71 @@ +--- +title: "Requirements" +description: "" +--- + +This page details the minimum requirements necessary for installing and using Infisical. +The actual resource requirements will vary in direct proportion to the operations performed by Infisical and the level of utilization by the end users. + + + +## Deployment Sizes + +**Small** suitable for most initial production setups, as well as development and testing scenarios. + +**Large** suitable for high-demand production environments, characterized by either a high volume of transactions, large number of secrets, or both. + + +## Hardware Requirements + +### Storage + +Infisical doesnโ€™t require file storage as all persisted data is saved in the database. +However, its logs and metrics are saved to disk for later viewing. As a result, we recommend provisioning 1-2 GB of storage. + +### CPU + +CPU requirements vary heavily on the volume of secret operations (reads and writes) you anticipate. +Processing large volumes of secrets frequently and consistently will require higher CPU. + +Recommended minimum CPU hardware for different sizes of deployments: + +- **small:**ย 2-4 core is theย **recommended**ย minimum +- **large:** 4-8 cores are suitable for larger deployments + +### Memory Allocation + +Memory needs depend on expected workload, including factors like user activity, automation level, and the frequency of secret operations. + +Recommended minimum memory hardware for different sizes of deployments: +- **small:**ย 4-8 GB is theย **recommended**ย minimum +- **large:** 16-32 GB are suitable for larger deployments + +## Database & caching layer + +### Postgres + +PostgreSQL is the only database supported by Infisical. Infisical has been extensively tested with Postgres version 16. We recommend using versions 14 and up for optimal compatibility. + +Recommended resource allocation based on deployment size: +- **small:**ย 1 vCPU / 2 GB RAM / 10 GB Disk +- **large:** 4vCPU / 16 GB RAM / 100 GB Disk + +### Redis + +Redis is utilized for session management and background tasks in Infisical. + +Redis requirements: + +- Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2. +- Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA). +- Redis storage needs are minimal: a setup with 1 vCPU, 1 GB RAM, and 1GB SSD will be sufficient for most deployments. + +## Supported Web Browsers + +Infisical supports a range of web browsers. However, features such as browser-based CLI login only work on Google Chrome and Firefox at the moment. + +- [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/new/) +- [Google Chrome](https://www.google.com/chrome/) +- [Chromium](https://www.chromium.org/getting-involved/dev-channel/) +- [Apple Safari](https://www.apple.com/safari/) +- [Microsoft Edge](https://www.microsoft.com/en-us/edge?form=MA13FJ) \ No newline at end of file diff --git a/docs/self-hosting/configuration/schema-migrations.mdx b/docs/self-hosting/configuration/schema-migrations.mdx new file mode 100644 index 000000000..6a94af751 --- /dev/null +++ b/docs/self-hosting/configuration/schema-migrations.mdx @@ -0,0 +1,60 @@ +--- +title: "Schema migration" +description: "Run Postgres schema migrations" +--- + +Running schema migrations is a requirement before deploying Infisical. +Each time you decide to upgrade your version of Infisical, it's necessary to run schema migrations for that specific version. +The guide below outlines a step-by-step guide to help you through this process. + +### Prerequisites +- Docker installed on your machine +- An active PostgreSQL database +- Postgres database connection string + + + + First, ensure you have the correct version of the Infisical Docker image. You can pull it from Docker Hub using the following command: + ```bash + docker pull infisical/infisical: + ``` + Replace `` with the specific version number you intend to deploy. View available versions [here](https://hub.docker.com/r/infisical/infisical/tags) + + + + The Docker image requires a `DB_CONNECTION_URI` environment variable. This connection string should point to your PostgreSQL database. The format generally looks like this: `postgresql://username:password@host:port/database`. + + + + To run the schema migration for the version of Infisical you want to deploy, use the following Docker command: + + ```bash + docker run --env DB_CONNECTION_URI= infisical/infisical: npm run migration:latest + ``` + Replace `` with your actual PostgreSQL connection string, and `` with the desired version number. + + + + After running the migration, it's good practice to check if the migration was successful. You can do this by checking the logs or accessing your database to ensure the schema has been updated accordingly. + + + If you need to rollback a migration by one step, use the following command: + + ```bash + docker run --env DB_CONNECTION_URI= infisical/infisical: npm run migration:rollback + ``` + + + + It's important to run schema migrations for each version of the Infisical you deploy. For instance, if you're updating from `infisical/infisical:1` to `infisical/infisical:2`, ensure you run the schema migrations for `infisical/infisical:2` before deploying it. + + + + + In a production setting, we recommend a more structured approach to deploying migrations prior to upgrading Infisical. This can be accomplished via CI automation. + + +### Additional discussion +- Always back up your database before running migrations, especially in a production environment. +- Test the migration process in a staging environment before applying it to production. +- Keep track of the versions and their corresponding migrations to avoid any inconsistencies. diff --git a/docs/self-hosting/configuration/sso.mdx b/docs/self-hosting/configuration/sso.mdx deleted file mode 100644 index 2d663790d..000000000 --- a/docs/self-hosting/configuration/sso.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: "Configure SSO" -description: "How to configure SSO when self-hosting Infisical." ---- - - - Infisical offers Google SSO and GitHub SSO for free. - - Infisical also offers SAML SSO authentication but as paid features that can be unlocked via enterprise license; if this is of interest, please contact team@infisical.com. - On this front, we currently support Okta, Azure AD, and JumpCloud and are expanding support for other IdPs in the coming months; stay tuned and feel free to request a IdP at this - [issue](https://github.com/Infisical/infisical/issues/442). - - -You can view specific documentation for how to set up each SSO authentication method below: - -- [Google SSO](/documentation/platform/sso/google) -- [GitHub SSO](/documentation/platform/sso/github) -- [GitLab SSO](/documentation/platform/sso/gitlab) -- [Okta SAML](/documentation/platform/sso/okta) -- [Azure SAML](/documentation/platform/sso/azure) -- [JumpCloud SAML](/documentation/platform/sso/jumpcloud) \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/docker-compose.mdx b/docs/self-hosting/deployment-options/docker-compose.mdx index 304fffbe3..cd67c60e9 100644 --- a/docs/self-hosting/deployment-options/docker-compose.mdx +++ b/docs/self-hosting/deployment-options/docker-compose.mdx @@ -2,53 +2,80 @@ title: "Docker Compose" description: "Run Infisical with Docker Compose template" --- +Install Infisical using Docker compose. This self hosting method contains all of the required components needed +to run a functional instance of Infisical. - - - ```bash - # Example in ubuntu - apt-get update - apt-get upgrade - apt install docker-compose - ``` - - - 2.1. Run the command below to download the `.env` file template. - - ```bash - wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example - ``` - - 2.2. Run the command below to download the docker compose template. - - ```bash - wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml - ``` - - 2.3. Run the command below to download the `nginx` config file. - - ```bash - mkdir nginx && wget -O ./nginx/default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf - ``` - - - - Running Infisical requires a few environment variables to be set. - At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL` which you can read more about [here](/self-hosting/configuration/envars). +## Prerequisites +- [Docker](https://docs.docker.com/engine/install/) +- [Docker compose](https://docs.docker.com/compose/install/) - Tweak the `.env` accordingly. + +This Docker Compose configuration is not designed for high-availability production scenarios. +It includes just the essential components needed to set up an Infisical proof of concept (POC). +Additional configuration is required to enhance data redundancy and ensure higher availability for production environments. + - ```bash - nano .env - ``` - - - Finally, run the command below to get Infisical up and running (in detached mode). +## Verify prerequisites + To verify that Docker compose and Docker are installed on the machine where you plan to install Infisical, run the following commands. + Check for docker installation ```bash - docker-compose -f docker-compose.yml up -d + docker ``` - Your Infisical installation is complete and should be running on port `80` or `http://localhost:80`. - - \ No newline at end of file + Check for docker compose installation + ```bash + docker-compose + ``` + +## Download docker compose file +You can obtain the Infisical docker compose file by using a command-line downloader such as `wget` or `curl`. +If your system doesn't have either of these, you can use a equivalent command that works with your machine. + + + + ```bash + curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + ``` + + + ```bash + wget -O docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + ``` + + + +## Configure instance credentials +Infisical requires a set of credentials used for connecting to dependent services such as Postgres, Redis, etc. +The default credentials can be downloaded using the one of the commands listed below. + + + + ```bash + curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example + ``` + + + ```bash + wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example + ``` + + + +Once downloaded, the credentials file will be saved to your working directly as `.env` file. +View all available configurations [here](/self-hosting/configuration/envars). + + + The default .env file contains credentials that are intended solely for testing purposes. + Please generate a new `ENCRYPTION_KEY` and `AUTH_SECRET` for use outside of testing. + Instructions to do so, can be found [here](/self-hosting/configuration/envars). + + +## Start Infisical +Run the command below to start Infisical and all related services. + +```bash +docker-compose -f docker-compose.prod.yml up +``` + +Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`. \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 5da640337..0386922d3 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -7,18 +7,29 @@ Prerequisites: - Basic knowledge of [Docker](https://www.docker.com/) - Have Docker installed on your system. If not, follow the installation guide [here](https://docs.docker.com/get-docker/). +Infisical is available as a single Docker image to ease deployment. +This Docker image only includes the application code, meaning you must supply a connection to a Postgres database and a Redis instance. +The following guide provides a detailed step-by-step walkthrough on how you can deploy Infisical with Docker. + - Run the following command in your terminal to pull the Infisical Docker image: + Visit [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) and select a version of Infisical image you would like to deploy. + Then run the following command in your terminal to pull the specific Infisical Docker image. ``` - docker pull infisical/infisical:latest + docker pull infisical/infisical: ``` + + Remember to replace `` with the docker image tag of your choice. + + + Before you can start the instance of Infisical, you need to run the database schema migrations. + Follow the step by [step guide here](/self-hosting/configuration/schema-migrations) on running schema migrations for Infisical. + - 2.1. Running Infisical requires a few environment variables to be set. - At minimum, Infisical requires that you set the variables `ENCRYPTION_KEY`, `AUTH_SECRET`, `MONGO_URL`, and `REDIS_URL` - which you can read more about [here](/self-hosting/configuration/envars). + For a minimal installation of Infisical, you must configure `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI`, and `REDIS_URL`. + [View all available configurations](/self-hosting/configuration/envars). Once you have added the required environment variables to your docker run command, execute it in your terminal to get Infisical up and running. @@ -28,22 +39,20 @@ Prerequisites: docker run -p 80:8080 \ -e ENCRYPTION_KEY=f40c9178624764ad85a6830b37ce239a \ -e AUTH_SECRET="q6LRi7c717a3DQ8JUxlWYkZpMhG4+RHLoFUVt3Bvo2U=" \ - -e MONGO_URL="<>" \ - infisical/infisical:latest + -e DB_CONNECTION_URI="<>" \ + -e REDIS_URL="<>" \ + infisical/infisical: ``` The above environment variable values are only to be used as an example and should not be used in production - 2.2. Once the container is running, verify the installation by opening your web browser and navigating to `http://localhost:80`. + Once the container is running, verify the installation by opening your web browser and navigating to `http://localhost:80`. - - - To have a functional deployment, we recommended compute with 2GB of RAM and 1 CPU. - - However, depending on your usage, you may need to further scale up system resources to meet demand. - - \ No newline at end of file +### Additional discussion +It's important to note that the above is a basic example of deploying Infisical using Docker. +In practice, for production deployments, you may want to use container orchestration platforms such as AWS ECS, Google Cloud Run, or Kubernetes. +These platforms offer additional features like scalability, load balancing, and automated deployment, making them suitable for handling production-level traffic and providing high availability. \ No newline at end of file diff --git a/docs/self-hosting/deployments/kubernetes.mdx b/docs/self-hosting/deployments/kubernetes.mdx deleted file mode 100644 index cbba7a946..000000000 --- a/docs/self-hosting/deployments/kubernetes.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: "Kubernetes" -description: "How to deploy Infisical with Kubernetes" ---- - - -Self-host vs. Infisical Cloud - -Self-hosting Infisical means managing the service yourself, taking care of upgrades, scaling, security, etc. - -If you're less technical and looking for a hands-free experience with minimal overhead then we recommend Infisical Cloud. - - - -**Prerequisites** -- You have understanding of [Kubernetes](https://kubernetes.io/) -- You have understanding of [Helm package manager](https://helm.sh/) -- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster - - -#### 1. Fill our environment variables - -Before you can deploy the Helm chart, you must fill out the required environment variables. To do so, please copy the below file to a `.yaml` file. -Refer to the available [environment variables](../../self-hosting/configuration/envars) to learn more - - -[View all available Helm chart values parameters](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical) -```yaml -frontend: - enabled: true - name: frontend - podAnnotations: {} - deploymentAnnotations: {} - replicaCount: 2 - image: - repository: infisical/frontend - tag: "latest" - pullPolicy: IfNotPresent - kubeSecretRef: "" - service: - annotations: {} - type: ClusterIP - nodePort: "" - -frontendEnvironmentVariables: - SITE_URL: infisical.local - -backend: - enabled: true - name: backend - podAnnotations: {} - deploymentAnnotations: {} - replicaCount: 2 - image: - repository: infisical/backend - tag: "latest" - pullPolicy: IfNotPresent - kubeSecretRef: "" - service: - annotations: {} - type: ClusterIP - nodePort: "" - -backendEnvironmentVariables: - ENCRYPTION_KEY: MUST_REPLACE - JWT_SIGNUP_SECRET: MUST_REPLACE - JWT_REFRESH_SECRET: MUST_REPLACE - JWT_AUTH_SECRET: MUST_REPLACE - JWT_SERVICE_SECRET: MUST_REPLACE - SMTP_HOST: MUST_REPLACE - SMTP_PORT: 587 - SMTP_SECURE: false - SMTP_FROM_NAME: Infisical - SMTP_FROM_ADDRESS: MUST_REPLACE - SMTP_USERNAME: MUST_REPLACE - SMTP_PASSWORD: MUST_REPLACE - SITE_URL: infisical.local - -## Mongo DB persistence -mongodb: - enabled: true - -## By default the backend will be connected to a Mongo instance within the cluster -## However, it is recommended to add a managed document DB connection string for production-use (DBaaS) -## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/ -## e.g. "mongodb://:@:/" -mongodbConnection: - externalMongoDBConnectionString: "" - -ingress: - enabled: true - annotations: - kubernetes.io/ingress.class: "nginx" - # cert-manager.io/issuer: letsencrypt-nginx - hostName: infisical.local ## <- Replace with your own domain - frontend: - path: / - pathType: Prefix - backend: - path: /api - pathType: Prefix - tls: [] - # - secretName: letsencrypt-nginx - # hosts: - # - infisical.local - -mailhog: - enabled: false -``` - - -Once you have a local copy of the values file, fill our the required environment variables and save the file. - - -#### 2. Install Infisical Helm repository - -```bash -helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' - -helm repo update -``` - -#### 3. Install the Helm chart - -By default, the helm chart will be installed on your default namespace. If you wish to install the Chart on a different namespace, you may specify -that by adding the `--namespace ` to your `helm install` command. - -```bash -## Installs to default namespace -helm install infisical-helm-charts/infisical --generate-name --values -``` - - -If you have not filled out all of the required environment variables, you will see an error message prompting you to -do so. - - -#### 4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`. \ No newline at end of file diff --git a/docs/self-hosting/deployments/linux.mdx b/docs/self-hosting/deployments/linux.mdx deleted file mode 100644 index d7490f870..000000000 --- a/docs/self-hosting/deployments/linux.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Linux VM" -description: "How to deploy Infisical with Docker-Compose" ---- - - -Self-host vs. Infisical Cloud - -Self-hosting Infisical means managing the service yourself, taking care of upgrades, scaling, security, etc. - -If you're less technical and looking for a hands-free experience with minimal overhead then we recommend Infisical Cloud. - - - -We provide a docker-compose deployment option for those who want to deploy Infisical onto a Linux VM easily. - -1. Install Docker on your VM - -```bash -# Example in ubuntu -apt-get update -apt-get upgrade -apt install docker-compose -``` - -2. Download the required files - -```bash -# Download env file template -wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example - -# Download docker compose template -wget -O docker-compose.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.yml - -# Download nginx config -mkdir nginx && wget -O ./nginx/default.conf https://raw.githubusercontent.com/Infisical/infisical/main/nginx/default.dev.conf -``` - -3. Tweak the `.env` according to your preferences. Refer to the available [environment variables](../../self-hosting/configuration/envars) - -```bash -# update environment variables like mongo login -nano .env -``` - -4. Get the service up and running. - -```bash -# Start up services in detached mode -docker-compose -f docker-compose.yml up -d -``` - -5. Your Infisical installation is complete and should be running on [http://localhost:80](http://localhost:80). Please note that the containers are not exposed to the internet and only bind to the localhost. It's up to you to configure a firewall, SSL certificates, and implement any additional security measures. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f28920f06..0e9e32a59 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "frontend", + "name": "npm-proj-1708142380787-0.9952765718063858vJAsWg", "lockfileVersion": 3, "requires": true, "packages": { @@ -48,7 +48,7 @@ "axios-auth-refresh": "^3.3.6", "base64-loader": "^1.0.0", "classnames": "^2.3.1", - "cookies": "^0.8.0", + "cookies": "^0.9.1", "cva": "npm:class-variance-authority@^0.4.0", "date-fns": "^2.30.0", "file-saver": "^2.0.5", @@ -68,7 +68,7 @@ "next": "^12.3.4", "nprogress": "^0.2.0", "picomatch": "^2.3.1", - "posthog-js": "^1.58.0", + "posthog-js": "^1.103.0", "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", @@ -84,7 +84,7 @@ "react-table": "^7.8.0", "sanitize-html": "^2.11.0", "set-cookie-parser": "^2.5.1", - "sharp": "^0.32.6", + "sharp": "^0.33.2", "styled-components": "^5.3.7", "tailwind-merge": "^1.8.1", "tweetnacl": "^1.0.3", @@ -94,7 +94,7 @@ "yaml": "^2.2.2", "yup": "^0.32.11", "zod": "^3.22.3", - "zustand": "^4.4.1" + "zustand": "^4.5.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.5.2", @@ -2487,6 +2487,15 @@ "react": ">=16.8.0" } }, + "node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -3243,6 +3252,437 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", + "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", + "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", + "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", + "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", + "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", + "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", + "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", + "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", + "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", + "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", + "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", + "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", + "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", + "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", + "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", + "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^0.45.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", + "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", + "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6574,6 +7014,29 @@ "node": ">=10" } }, + "node_modules/@storybook/nextjs/node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@storybook/nextjs/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -8939,9 +9402,10 @@ } }, "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", @@ -9240,6 +9704,43 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.0.tgz", + "integrity": "sha512-Yyyqff4PIFfSuthCZqLlPISTWHmnQxoPuAvkmgzsJEmG3CesdIv6Xweayl0JkCZJSB2yYIdJyEz97tpxNhgjbg==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.1.5.tgz", + "integrity": "sha512-5t0nlecX+N2uJqdxe9d18A98cp2u9BETelbjKpiVgQqzzmVNFYWEAjQHqS+2Khgto1vcwhik9cXucaj5ve2WWA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-os": "^2.0.0", + "bare-path": "^2.0.0", + "streamx": "^2.13.0" + } + }, + "node_modules/bare-os": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.0.tgz", + "integrity": "sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.0.tgz", + "integrity": "sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -9253,6 +9754,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -9336,6 +9838,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -9346,6 +9849,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -9624,6 +10128,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -10351,9 +10856,9 @@ "dev": true }, "node_modules/cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", "dependencies": { "depd": "~2.0.0", "keygrip": "~1.1.0" @@ -10941,6 +11446,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -10993,6 +11499,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, "engines": { "node": ">=4.0.0" } @@ -11634,6 +12141,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, "dependencies": { "once": "^1.4.0" } @@ -12708,6 +13216,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, "engines": { "node": ">=6" } @@ -12849,7 +13358,8 @@ "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true }, "node_modules/fast-glob": { "version": "3.3.2", @@ -13435,7 +13945,8 @@ "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true }, "node_modules/fs-extra": { "version": "11.2.0", @@ -13686,7 +14197,8 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true }, "node_modules/github-slugger": { "version": "1.5.0", @@ -14412,6 +14924,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -16856,6 +17369,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -16959,7 +17473,8 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true }, "node_modules/mri": { "version": "1.2.0", @@ -17008,7 +17523,8 @@ "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -17185,6 +17701,7 @@ "version": "3.54.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.54.0.tgz", "integrity": "sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==", + "dev": true, "dependencies": { "semver": "^7.3.5" }, @@ -17196,6 +17713,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -17204,9 +17722,10 @@ } }, "node_modules/node-abi/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -17220,7 +17739,8 @@ "node_modules/node-abi/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/node-abort-controller": { "version": "3.1.1", @@ -17231,7 +17751,8 @@ "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true }, "node_modules/node-dir": { "version": "0.1.17", @@ -18544,17 +19065,28 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/posthog-js": { - "version": "1.100.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.100.0.tgz", - "integrity": "sha512-r2XZEiHQ9mBK7D1G9k57I8uYZ2kZTAJ0OCX6K/OOdCWN8jKPhw3h5F9No5weilP6eVAn+hrsy7NvPV7SCX7gMg==", + "version": "1.103.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.103.0.tgz", + "integrity": "sha512-NldabkbCB9a/2JLszoB7vk5XZr93iBoGEgEEKn201oDD3QkRn1nC+c+e1HQ3S9oMs5oZIhKmPNBCje1J9prYRg==", "dependencies": { - "fflate": "^0.4.1" + "fflate": "^0.4.8", + "preact": "^10.19.3" + } + }, + "node_modules/preact": { + "version": "10.19.5", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz", + "integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" } }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -18579,12 +19111,14 @@ "node_modules/prebuild-install/node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true }, "node_modules/prebuild-install/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -18598,6 +19132,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -18609,6 +19144,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -18911,6 +19447,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -19149,7 +19686,8 @@ "node_modules/queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true }, "node_modules/quick-lru": { "version": "5.1.1", @@ -19233,6 +19771,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -19246,12 +19785,14 @@ "node_modules/rc/node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -20820,25 +21361,42 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, "node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", + "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" + "semver": "^7.5.4" }, "engines": { - "node": ">=14.15.0" + "libvips": ">=8.15.1", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.2", + "@img/sharp-darwin-x64": "0.33.2", + "@img/sharp-libvips-darwin-arm64": "1.0.1", + "@img/sharp-libvips-darwin-x64": "1.0.1", + "@img/sharp-libvips-linux-arm": "1.0.1", + "@img/sharp-libvips-linux-arm64": "1.0.1", + "@img/sharp-libvips-linux-s390x": "1.0.1", + "@img/sharp-libvips-linux-x64": "1.0.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", + "@img/sharp-libvips-linuxmusl-x64": "1.0.1", + "@img/sharp-linux-arm": "0.33.2", + "@img/sharp-linux-arm64": "0.33.2", + "@img/sharp-linux-s390x": "0.33.2", + "@img/sharp-linux-x64": "0.33.2", + "@img/sharp-linuxmusl-arm64": "0.33.2", + "@img/sharp-linuxmusl-x64": "0.33.2", + "@img/sharp-wasm32": "0.33.2", + "@img/sharp-win32-ia32": "0.33.2", + "@img/sharp-win32-x64": "0.33.2" } }, "node_modules/sharp/node_modules/lru-cache": { @@ -20916,6 +21474,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, "funding": [ { "type": "github", @@ -20935,6 +21494,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, "funding": [ { "type": "github", @@ -21318,12 +21878,16 @@ "dev": true }, "node_modules/streamx": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", - "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", + "version": "2.15.8", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.8.tgz", + "integrity": "sha512-6pwMeMY/SuISiRsuS8TeIrAzyFbG5gGPHFQsYjUr/pbBadaL1PCWmzKw+CHZSwainfvcF6Si6cVLq4XTEwswFQ==", + "dev": true, "dependencies": { "fast-fifo": "^1.1.0", "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/strict-uri-encode": { @@ -21786,19 +22350,24 @@ } }, "node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "dev": true, "dependencies": { - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, "node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -22322,6 +22891,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -23536,9 +24106,9 @@ } }, "node_modules/zustand": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", - "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz", + "integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==", "dependencies": { "use-sync-external-store": "1.2.0" }, @@ -23547,7 +24117,7 @@ }, "peerDependencies": { "@types/react": ">=16.8", - "immer": ">=9.0", + "immer": ">=9.0.6", "react": ">=16.8" }, "peerDependenciesMeta": { diff --git a/frontend/package.json b/frontend/package.json index 53e383f42..3407af2d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -56,7 +56,7 @@ "axios-auth-refresh": "^3.3.6", "base64-loader": "^1.0.0", "classnames": "^2.3.1", - "cookies": "^0.8.0", + "cookies": "^0.9.1", "cva": "npm:class-variance-authority@^0.4.0", "date-fns": "^2.30.0", "file-saver": "^2.0.5", @@ -76,7 +76,7 @@ "next": "^12.3.4", "nprogress": "^0.2.0", "picomatch": "^2.3.1", - "posthog-js": "^1.58.0", + "posthog-js": "^1.103.0", "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", @@ -92,7 +92,7 @@ "react-table": "^7.8.0", "sanitize-html": "^2.11.0", "set-cookie-parser": "^2.5.1", - "sharp": "^0.32.6", + "sharp": "^0.33.2", "styled-components": "^5.3.7", "tailwind-merge": "^1.8.1", "tweetnacl": "^1.0.3", @@ -102,7 +102,7 @@ "yaml": "^2.2.2", "yup": "^0.32.11", "zod": "^3.22.3", - "zustand": "^4.4.1" + "zustand": "^4.5.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.5.2", diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 9f109b931..8ad06375b 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -59,8 +59,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa const workspaceId = router.query.id as string; // Delete the row in the table (e.g. a user) // #TODO: Add a pop-up that warns you that the user is going to be deleted. - const handleDelete = async (membershipId: string) => { - await deleteUserFromWorkspaceMutateAsync({ membershipId, workspaceId }); + const handleDelete = async (email: string) => { + await deleteUserFromWorkspaceMutateAsync({ emails: [email], workspaceId }); }; const handleRoleUpdate = async (index: number, e: string) => { @@ -368,7 +368,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa myRole !== "member" ? (
+
+ + ); +}; diff --git a/frontend/src/components/v2/UpgradeProjectAlert/index.tsx b/frontend/src/components/v2/UpgradeProjectAlert/index.tsx new file mode 100644 index 000000000..ab67f86cf --- /dev/null +++ b/frontend/src/components/v2/UpgradeProjectAlert/index.tsx @@ -0,0 +1 @@ +export { UpgradeProjectAlert } from "./UpgradeProjectAlert"; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index f0fbd23eb..8a7393a17 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -13,6 +13,7 @@ export enum OrgPermissionSubjects { Member = "member", Settings = "settings", IncidentAccount = "incident-contact", + Scim = "scim", Sso = "sso", Billing = "billing", SecretScanning = "secret-scanning", @@ -26,6 +27,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] + | [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 21f73e4c6..06f13dc65 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -1,10 +1,5 @@ -import crypto from "crypto"; - -import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets"; -import { uploadWsKey } from "@app/hooks/api/keys/queries"; import { createSecret } from "@app/hooks/api/secrets/mutations"; -import { fetchUserDetails } from "@app/hooks/api/users/queries"; import { createWorkspace } from "@app/hooks/api/workspace/queries"; const secretsToBeAdded = [ @@ -91,43 +86,22 @@ const initProjectHelper = async ({ }) => { // create new project const { - data: { workspace } + data: { project } } = await createWorkspace({ - workspaceName: projectName, - organizationId - }); - - // create and upload new (encrypted) project key - const randomBytes = crypto.randomBytes(16).toString("hex"); - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - if (!PRIVATE_KEY) throw new Error("Failed to find private key"); - - const user = await fetchUserDetails(); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: randomBytes, - publicKey: user.publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey({ - workspaceId: workspace.id, - userId: user.id, - encryptedKey: ciphertext, - nonce + organizationId, + projectName }); // encrypt and upload secrets to new project const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, - workspaceId: workspace.id, + workspaceId: project.id, env: "dev" }); secrets?.forEach((secret) => { createSecret({ - workspaceId: workspace.id, + workspaceId: project.id, environment: secret.environment, type: secret.type, secretKey: secret.secretName, @@ -147,7 +121,7 @@ const initProjectHelper = async ({ }); }); - return workspace; + return project; }; export { initProjectHelper }; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index ea53ff1db..6d25944ef 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { organizationKeys } from "../organization/queries"; import { User } from "../users/types"; import { adminQueryKeys } from "./queries"; import { TCreateAdminUserDTO, TServerConfig } from "./types"; @@ -9,7 +10,11 @@ import { TCreateAdminUserDTO, TServerConfig } from "./types"; export const useCreateAdminUser = () => { const queryClient = useQueryClient(); - return useMutation<{ user: User; token: string }, {}, TCreateAdminUserDTO>({ + return useMutation< + { user: User; token: string; organization: { id: string } }, + {}, + TCreateAdminUserDTO + >({ mutationFn: async (opt) => { const { data } = await apiRequest.post("/api/v1/admin/signup", opt); return data; @@ -34,6 +39,7 @@ export const useUpdateServerConfig = () => { onSuccess: (data) => { queryClient.setQueryData(adminQueryKeys.serverConfig(), data); queryClient.invalidateQueries(adminQueryKeys.serverConfig()); + queryClient.invalidateQueries(organizationKeys.getUserOrganizations); } }); }; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 4caeaea48..d63a5e111 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -10,6 +10,7 @@ export * from "./integrations"; export * from "./keys"; export * from "./organization"; export * from "./roles"; +export * from "./scim"; export * from "./secretApproval"; export * from "./secretApprovalRequest"; export * from "./secretFolders"; diff --git a/frontend/src/hooks/api/keys/queries.tsx b/frontend/src/hooks/api/keys/queries.tsx index 3cf11e189..7e59c0840 100644 --- a/frontend/src/hooks/api/keys/queries.tsx +++ b/frontend/src/hooks/api/keys/queries.tsx @@ -8,9 +8,9 @@ const encKeyKeys = { getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const }; -export const fetchUserWsKey = async (workspaceID: string) => { +export const fetchUserWsKey = async (projectId: string) => { const { data } = await apiRequest.get( - `/api/v2/workspace/${workspaceID}/encrypted-key` + `/api/v2/workspace/${projectId}/encrypted-key` ); return data; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 018fa1edb..441aa591c 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -71,12 +71,14 @@ export const useUpdateOrg = () => { mutationFn: ({ name, authEnforced, + scimEnabled, slug, orgId }) => { return apiRequest.patch(`/api/v1/organization/${orgId}`, { name, authEnforced, + scimEnabled, slug }); }, diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 6492e3515..9c26ce02b 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -4,6 +4,7 @@ export type Organization = { createAt: string; updatedAt: string; authEnforced: boolean; + scimEnabled: boolean; slug: string; }; @@ -11,6 +12,7 @@ export type UpdateOrgDTO = { orgId: string; name?: string; authEnforced?: boolean; + scimEnabled?: boolean; slug?: string; }; diff --git a/frontend/src/hooks/api/scim/index.tsx b/frontend/src/hooks/api/scim/index.tsx new file mode 100644 index 000000000..1981659d1 --- /dev/null +++ b/frontend/src/hooks/api/scim/index.tsx @@ -0,0 +1,5 @@ +export { + useCreateScimToken, + useDeleteScimToken +} from "./mutations"; +export { useGetScimTokens } from "./queries"; \ No newline at end of file diff --git a/frontend/src/hooks/api/scim/mutations.tsx b/frontend/src/hooks/api/scim/mutations.tsx new file mode 100644 index 000000000..7469fb532 --- /dev/null +++ b/frontend/src/hooks/api/scim/mutations.tsx @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { scimKeys } from "./queries"; +import { + CreateScimTokenDTO, + CreateScimTokenRes, + DeleteScimTokenDTO +} from "./types"; + +export const useCreateScimToken = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + organizationId, + description, + ttlDays + }) => { + const { data } = await apiRequest.post("/api/v1/scim/scim-tokens", { + organizationId, + description, + ttlDays + }); + + return data; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(scimKeys.getScimTokens(organizationId)); + } + }); +}; + +export const useDeleteScimToken = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ scimTokenId }) => { + const { data } = await apiRequest.delete(`/api/v1/scim/scim-tokens/${scimTokenId}`); + return data; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(scimKeys.getScimTokens(organizationId)); + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/scim/queries.tsx b/frontend/src/hooks/api/scim/queries.tsx new file mode 100644 index 000000000..f76018c01 --- /dev/null +++ b/frontend/src/hooks/api/scim/queries.tsx @@ -0,0 +1,25 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { ScimTokenData } from "./types"; + +export const scimKeys = { + getScimTokens: (orgId: string) => [{ orgId }, "organization-scim-token"] as const, +}; + +export const useGetScimTokens = (organizationId: string) => { + return useQuery({ + queryKey: scimKeys.getScimTokens(organizationId), + queryFn: async () => { + if (organizationId === "") { + return undefined; + } + + const { data: { scimTokens } } = await apiRequest.get<{ scimTokens: ScimTokenData[] }>(`/api/v1/scim/scim-tokens?organizationId=${organizationId}`); + + return scimTokens; + }, + enabled: true + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/scim/types.ts b/frontend/src/hooks/api/scim/types.ts new file mode 100644 index 000000000..a373ba943 --- /dev/null +++ b/frontend/src/hooks/api/scim/types.ts @@ -0,0 +1,24 @@ +export type ScimTokenData = { + id: string; + ttlDays: number; + description: string; + tokenSuffix: string; + orgId: string; + createdAt: string; + updatedAt: string; +}; + +export type CreateScimTokenDTO = { + organizationId: string; + description?: string; + ttlDays?: number; +} + +export type DeleteScimTokenDTO = { + organizationId: string; + scimTokenId: string; +} + +export type CreateScimTokenRes = { + scimToken: string; +} \ No newline at end of file diff --git a/frontend/src/hooks/api/ssoConfig/queries.tsx b/frontend/src/hooks/api/ssoConfig/queries.tsx index 9b75095b8..c294679f8 100644 --- a/frontend/src/hooks/api/ssoConfig/queries.tsx +++ b/frontend/src/hooks/api/ssoConfig/queries.tsx @@ -1,6 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { organizationKeys } from "@app/hooks/api/organization/queries"; const ssoConfigKeys = { getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const @@ -82,8 +83,12 @@ export const useUpdateSSOConfig = () => { return data; }, - onSuccess(_, dto) { - queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId)); + onSuccess(_, { organizationId, isActive }) { + if (isActive === false) { + queryClient.invalidateQueries(organizationKeys.getUserOrganizations); + } + + queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(organizationId)); } }); }; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index b975f0472..5680da02d 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -18,6 +18,7 @@ export type SubscriptionPlan = { workspacesUsed: number; environmentLimit: number; samlSSO: boolean; + scim: boolean; status: | "incomplete" | "incomplete_expired" diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 04d215898..6748db6ac 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -22,7 +22,7 @@ export * from "./secrets/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { SubscriptionPlan } from "./subscriptions/types"; export type { WsTag } from "./tags/types"; -export type { AddUserToWsDTO, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types"; +export type { AddUserToWsDTOE2EE, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types"; export type { TWebhook } from "./webhooks/types"; export type { CreateEnvironmentDTO, diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index 75dd36cc5..9c51948f2 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -1,4 +1,4 @@ -export { useAddUserToWs } from "./mutation"; +export { useAddUserToWsE2EE, useAddUserToWsNonE2EE } from "./mutation"; export { fetchOrgUsers, useAddUserToOrg, diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index 03276c2ef..7d982cea1 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -7,12 +7,12 @@ import { import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; -import { AddUserToWsDTO } from "./types"; +import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types"; -export const useAddUserToWs = () => { +export const useAddUserToWsE2EE = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, AddUserToWsDTO>({ + return useMutation<{}, {}, AddUserToWsDTOE2EE>({ mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => { // assymmetrically decrypt symmetric key with local private key const key = decryptAssymmetric({ @@ -45,3 +45,19 @@ export const useAddUserToWs = () => { } }); }; + +export const useAddUserToWsNonE2EE = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, AddUserToWsDTONonE2EE>({ + mutationFn: async ({ projectId, emails }) => { + const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, { + emails + }); + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(projectId)); + } + }); +}; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index ebddefb14..366ee93bf 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -63,7 +63,7 @@ export type TProjectMembership = { export type TWorkspaceUser = OrgUser; -export type AddUserToWsDTO = { +export type AddUserToWsDTOE2EE = { workspaceId: string; decryptKey: UserWsKeyPair; userPrivateKey: string; @@ -73,6 +73,11 @@ export type AddUserToWsDTO = { }[]; }; +export type AddUserToWsDTONonE2EE = { + projectId: string; + emails: string[]; +}; + export type UpdateOrgUserRoleDTO = { organizationId: string; membershipId: string; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index 63be0f7ad..b96223077 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -1,12 +1,12 @@ export { useAddIdentityToWorkspace, - useAddUserToWorkspace, useCreateWorkspace, useCreateWsEnvironment, useDeleteIdentityFromWorkspace, useDeleteUserFromWorkspace, useDeleteWorkspace, useDeleteWsEnvironment, + useGetUpgradeProjectStatus, useGetUserWorkspaceMemberships, useGetUserWorkspaces, useGetWorkspaceAuthorizations, @@ -21,5 +21,6 @@ export { useToggleAutoCapitalization, useUpdateIdentityWorkspaceRole, useUpdateUserWorkspaceRole, - useUpdateWsEnvironment + useUpdateWsEnvironment, + useUpgradeProject } from "./queries"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index f47a77537..d970cab54 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -14,6 +14,7 @@ import { DeleteWorkspaceDTO, NameWorkspaceSecretsDTO, RenameWorkspaceDTO, + TGetUpgradeProjectStatusDTO, ToggleAutoCapitalizationDTO, UpdateEnvironmentDTO, Workspace @@ -24,6 +25,7 @@ export const workspaceKeys = { getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const, getWorkspaceIndexStatus: (workspaceId: string) => [{ workspaceId }, "workspace-index-status"] as const, + getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"], getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"], getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"], getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], @@ -51,6 +53,14 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => { return data; }; +const fetchProjectUpgradeStatus = async (projectId: string) => { + const { data } = await apiRequest.get<{ status: string }>( + `/api/v2/workspace/${projectId}/upgrade/status` + ); + + return data; +}; + export const fetchWorkspaceSecrets = async (workspaceId: string) => { const { data: { secrets } @@ -61,6 +71,36 @@ export const fetchWorkspaceSecrets = async (workspaceId: string) => { return secrets; }; +export const useUpgradeProject = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, { projectId: string; privateKey: string }>({ + mutationFn: ({ projectId, privateKey }) => { + return apiRequest.post(`/api/v2/workspace/${projectId}/upgrade`, { + userPrivateKey: privateKey + }); + }, + onSuccess: () => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + } + }); +}; + +export const useGetUpgradeProjectStatus = ({ + projectId, + onSuccess, + enabled, + refetchInterval +}: TGetUpgradeProjectStatusDTO) => { + return useQuery({ + queryKey: workspaceKeys.getProjectUpgradeStatus(projectId), + queryFn: () => fetchProjectUpgradeStatus(projectId), + enabled, + onSuccess, + refetchInterval + }); +}; + const fetchUserWorkspaces = async () => { const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace"); return data.workspaces; @@ -158,19 +198,19 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) => export const createWorkspace = ({ organizationId, - workspaceName -}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => { - return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId }); + projectName +}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => { + return apiRequest.post("/api/v2/workspace", { projectName, organizationId }); }; export const useCreateWorkspace = () => { const queryClient = useQueryClient(); - return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({ - mutationFn: async ({ organizationId, workspaceName }) => + return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({ + mutationFn: async ({ organizationId, projectName }) => createWorkspace({ organizationId, - workspaceName + projectName }), onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); @@ -279,40 +319,16 @@ export const useGetWorkspaceUsers = (workspaceId: string) => { }); }; -export const useAddUserToWorkspace = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => { - const { - data: { invitee, latestKey } - } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }); - - return { - invitee, - latestKey - }; - }, - onSuccess: (_, dto) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId)); - } - }); -}; - export const useDeleteUserFromWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - membershipId, - workspaceId - }: { - workspaceId: string; - membershipId: string; - }) => { + mutationFn: async ({ emails, workspaceId }: { workspaceId: string; emails: string[] }) => { const { data: { deletedMembership } - } = await apiRequest.delete(`/api/v1/workspace/${workspaceId}/memberships/${membershipId}`); + } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/memberships`, { + data: { emails } + }); return deletedMembership; }, onSuccess: (_, { workspaceId }) => { diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 10d222a2a..c4aada862 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -1,10 +1,18 @@ +export enum ProjectVersion { + V1 = 1, + V2 = 2 +} + export type Workspace = { __v: number; id: string; name: string; orgId: string; + version: ProjectVersion; + upgradeStatus: string | null; autoCapitalization: boolean; environments: WorkspaceEnv[]; + slug: string; }; export type WorkspaceEnv = { @@ -23,9 +31,16 @@ export type NameWorkspaceSecretsDTO = { }[]; }; +export type TGetUpgradeProjectStatusDTO = { + projectId: string; + onSuccess?: (data?: { status: string }) => void; + enabled?: boolean; + refetchInterval?: number; +}; + // mutation dto export type CreateWorkspaceDTO = { - workspaceName: string; + projectName: string; organizationId: string; }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index d175afe48..8957f6d91 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -4,7 +4,6 @@ /* eslint-disable vars-on-top */ /* eslint-disable no-var */ /* eslint-disable func-names */ -import crypto from "crypto"; import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; @@ -34,7 +33,6 @@ import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { OrgPermissionCan } from "@app/components/permissions"; import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage"; -import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import { Button, Checkbox, @@ -51,6 +49,7 @@ import { SelectItem, UpgradePlanModal } from "@app/components/v2"; +import { UpgradeOverlay } from "@app/components/v2/UpgradeOverlay"; import { OrgPermissionActions, OrgPermissionSubjects, @@ -62,16 +61,14 @@ import { import { usePopUp } from "@app/hooks"; import { fetchOrgUsers, - useAddUserToWs, + useAddUserToWsNonE2EE, useCreateWorkspace, useGetOrgTrialUrl, useGetSecretApprovalRequestCount, useGetUserAction, useLogoutUser, - useRegisterUserAction, - useUploadWsKey + useRegisterUserAction } from "@app/hooks/api"; -import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; import { CreateOrgModal } from "@app/views/Org/components"; interface LayoutProps { @@ -129,8 +126,8 @@ export const AppLayout = ({ children }: LayoutProps) => { : true; const createWs = useCreateWorkspace(); - const uploadWsKey = useUploadWsKey(); - const addWsUser = useAddUserToWs(); + const addUsersToProject = useAddUserToWsNonE2EE(); + const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -220,59 +217,44 @@ export const AppLayout = ({ children }: LayoutProps) => { const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { // type check - if (!currentOrg?.id) return; + if (!currentOrg) return; + if (!user) return; try { const { data: { - workspace: { id: newWorkspaceId } + project: { id: newProjectId } } } = await createWs.mutateAsync({ - organizationId: currentOrg?.id, - workspaceName: name - }); - - const randomBytes = crypto.randomBytes(16).toString("hex"); - const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY")); - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: randomBytes, - publicKey: user.publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey.mutateAsync({ - encryptedKey: ciphertext, - nonce, - userId: user?.id, - workspaceId: newWorkspaceId + organizationId: currentOrg.id, + projectName: name }); if (addMembers) { - // not using hooks because need at this point only const orgUsers = await fetchOrgUsers(currentOrg.id); - const decryptKey = await fetchUserWsKey(newWorkspaceId); - await addWsUser.mutateAsync({ - workspaceId: newWorkspaceId, - decryptKey, - userPrivateKey: PRIVATE_KEY, - members: orgUsers - .filter( - ({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email - ) - .map(({ user: orgUser, id: orgMembershipId }) => ({ - userPublicKey: orgUser.publicKey, - orgMembershipId - })) + + await addUsersToProject.mutateAsync({ + emails: orgUsers + .map((member) => member.user.email) + .filter((email) => email !== user.email), + projectId: newProjectId }); } + + // eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected. + await new Promise((resolve) => setTimeout(resolve, 2_000)); + + // eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected. + await new Promise((resolve) => setTimeout(resolve, 2_000)); + createNotification({ text: "Workspace created", type: "success" }); handlePopUpClose("addNewWs"); - router.push(`/project/${newWorkspaceId}/secrets/overview`); + router.push(`/project/${newProjectId}/secrets/overview`); } catch (err) { console.error(err); createNotification({ text: "Failed to create workspace", type: "error" }); } }; - + return ( <>
@@ -280,6 +262,7 @@ export const AppLayout = ({ children }: LayoutProps) => {