diff --git a/.env.example b/.env.example index 4029cb141c..8463fea927 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 # Required -DB_CONNECTION_URI=postgres://infisical:infisical@db:5432/infisical +DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} # JWT # Required secrets to sign JWT tokens @@ -19,10 +19,6 @@ POSTGRES_DB=infisical # Redis REDIS_URL=redis://redis:6379 -# Optional credentials for MongoDB container instance and Mongo-Express -MONGO_USERNAME=root -MONGO_PASSWORD=example - # Website URL # Required SITE_URL=http://localhost:8080 diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 0000000000..bce047f77d --- /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/resources/changelog-generator.py b/.github/resources/changelog-generator.py new file mode 100644 index 0000000000..7dd8140eee --- /dev/null +++ b/.github/resources/changelog-generator.py @@ -0,0 +1,190 @@ +# inspired by https://www.photoroom.com/inside-photoroom/how-we-automated-our-changelog-thanks-to-chatgpt +import os +import requests +import re +from openai import OpenAI +import subprocess +from datetime import datetime + +import uuid + +# Constants +REPO_OWNER = "infisical" +REPO_NAME = "infisical" +TOKEN = os.environ["GITHUB_TOKEN"] +SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"] +OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] +SLACK_MSG_COLOR = "#36a64f" + +headers = { + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", +} + + +def set_multiline_output(name, value): + with open(os.environ['GITHUB_OUTPUT'], 'a') as fh: + delimiter = uuid.uuid1() + print(f'{name}<<{delimiter}', file=fh) + print(value, file=fh) + print(delimiter, file=fh) + +def post_changelog_to_slack(changelog, tag): + slack_payload = { + "text": "Hey team, it's changelog time! :wave:", + "attachments": [ + { + "color": SLACK_MSG_COLOR, + "title": f"πŸ—“οΈInfisical Changelog - {tag}", + "text": changelog, + } + ], + } + + response = requests.post(SLACK_WEBHOOK_URL, json=slack_payload) + + if response.status_code != 200: + raise Exception("Failed to post changelog to Slack.") + +def find_previous_release_tag(release_tag:str): + previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{release_tag}^"]).decode("utf-8").strip() + while not(previous_tag.startswith("infisical/")): + previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{previous_tag}^"]).decode("utf-8").strip() + return previous_tag + +def get_tag_creation_date(tag_name): + url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/git/refs/tags/{tag_name}" + response = requests.get(url, headers=headers) + response.raise_for_status() + commit_sha = response.json()['object']['sha'] + + commit_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/commits/{commit_sha}" + commit_response = requests.get(commit_url, headers=headers) + commit_response.raise_for_status() + creation_date = commit_response.json()['commit']['author']['date'] + + return datetime.strptime(creation_date, '%Y-%m-%dT%H:%M:%SZ') + + +def fetch_prs_between_tags(previous_tag_date:datetime, release_tag_date:datetime): + # Use GitHub API to fetch PRs merged between the commits + url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/pulls?state=closed&merged=true" + response = requests.get(url, headers=headers) + + if response.status_code != 200: + raise Exception("Error fetching PRs from GitHub API!") + + prs = [] + for pr in response.json(): + # the idea is as tags happen recently we get last 100 closed PRs and then filter by tag creation date + if pr["merged_at"] and datetime.strptime(pr["merged_at"],'%Y-%m-%dT%H:%M:%SZ') < release_tag_date and datetime.strptime(pr["merged_at"],'%Y-%m-%dT%H:%M:%SZ') > previous_tag_date: + prs.append(pr) + + return prs + + +def extract_commit_details_from_prs(prs): + commit_details = [] + for pr in prs: + commit_message = pr["title"] + commit_url = pr["html_url"] + pr_number = pr["number"] + branch_name = pr["head"]["ref"] + issue_numbers = re.findall(r"(www-\d+|web-\d+)", branch_name) + + # If no issue numbers are found, add the PR details without issue numbers and URLs + if not issue_numbers: + commit_details.append( + { + "message": commit_message, + "pr_number": pr_number, + "pr_url": commit_url, + "issue_number": None, + "issue_url": None, + } + ) + continue + + for issue in issue_numbers: + commit_details.append( + { + "message": commit_message, + "pr_number": pr_number, + "pr_url": commit_url, + "issue_number": issue, + } + ) + + return commit_details + +# Function to generate changelog using OpenAI +def generate_changelog_with_openai(commit_details): + commit_messages = [] + for details in commit_details: + base_message = f"{details['pr_url']} - {details['message']}" + # Add the issue URL if available + # if details["issue_url"]: + # base_message += f" (Linear Issue: {details['issue_url']})" + commit_messages.append(base_message) + + commit_list = "\n".join(commit_messages) + prompt = """ +Generate a changelog for Infisical, opensource secretops +The changelog should: +1. Be Informative: Using the provided list of GitHub commits, break them down into categories such as Features, Fixes & Improvements, and Technical Updates. Summarize each commit concisely, ensuring the key points are highlighted. +2. Have a Professional yet Friendly tone: The tone should be balanced, not too corporate or too informal. +3. Celebratory Introduction and Conclusion: Start the changelog with a celebratory note acknowledging the team's hard work and progress. End with a shoutout to the team and wishes for a pleasant weekend. +4. Formatting: you cannot use Markdown formatting, and you can only use emojis for the introductory paragraph or the conclusion paragraph, nowhere else. +5. Links: the syntax to create links is the following: ``. +6. Linear Links: note that the Linear link is optional, include it only if provided. +7. Do not wrap your answer in a codeblock. Just output the text, nothing else +Here's a good example to follow, please try to match the formatting as closely as possible, only changing the content of the changelog and have some liberty with the introduction. Notice the importance of the formatting of a changelog item: +- : We optimize our ci to strip comments and minify production builds. ()) +And here's an example of the full changelog: + +*Features* +β€’ : We optimize our ci to strip comments and minify production builds. () +*Fixes & Improvements* +β€’ : We optimize our ci to strip comments and minify production builds. () +*Technical Updates* +β€’ : We optimize our ci to strip comments and minify production builds. () + +Stay tuned for more exciting updates coming soon! +And here are the commits: +{} + """.format( + commit_list + ) + + client = OpenAI(api_key=OPENAI_API_KEY) + messages = [{"role": "user", "content": prompt}] + response = client.chat.completions.create(model="gpt-3.5-turbo", messages=messages) + + if "error" in response.choices[0].message: + raise Exception("Error generating changelog with OpenAI!") + + return response.choices[0].message.content.strip() + + +if __name__ == "__main__": + try: + # Get the latest and previous release tags + latest_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]).decode("utf-8").strip() + previous_tag = find_previous_release_tag(latest_tag) + + latest_tag_date = get_tag_creation_date(latest_tag) + previous_tag_date = get_tag_creation_date(previous_tag) + + prs = fetch_prs_between_tags(previous_tag_date,latest_tag_date) + pr_details = extract_commit_details_from_prs(prs) + + # Generate changelog + changelog = generate_changelog_with_openai(pr_details) + + post_changelog_to_slack(changelog,latest_tag) + # Print or post changelog to Slack + # set_multiline_output("changelog", changelog) + + except Exception as e: + print(str(e)) \ No newline at end of file diff --git a/.github/values.yaml b/.github/values.yaml index 90bf2ce0a8..1b3ffd87aa 100644 --- a/.github/values.yaml +++ b/.github/values.yaml @@ -13,11 +13,10 @@ fullnameOverride: "" ## infisical: - ## @param backend.enabled Enable backend - ## + autoDatabaseSchemaMigration: false + enabled: false - ## @param backend.name Backend name - ## + name: infisical replicaCount: 3 image: @@ -28,7 +27,7 @@ infisical: deploymentAnnotations: secrets.infisical.com/auto-reload: "true" - kubeSecretRef: "infisical-gamma-secrets" + kubeSecretRef: "managed-secret" ingress: ## @param ingress.enabled Enable ingress @@ -50,3 +49,9 @@ ingress: - secretName: letsencrypt-prod hosts: - gamma.infisical.com + +postgresql: + enabled: false + +redis: + enabled: false diff --git a/.github/workflows/check-api-for-breaking-changes.yml b/.github/workflows/check-api-for-breaking-changes.yml index 39c9c6fb7f..7fba6c321a 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 75465a0144..17e5a9d745 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/generate-release-changelog.yml b/.github/workflows/generate-release-changelog.yml new file mode 100644 index 0000000000..e26a304cfb --- /dev/null +++ b/.github/workflows/generate-release-changelog.yml @@ -0,0 +1,34 @@ +name: Generate Changelog +permissions: + contents: write + +on: + workflow_dispatch: + push: + tags: + - "infisical/v*.*.*-postgres" + +jobs: + generate_changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12.0" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests openai + - name: Generate Changelog and Post to Slack + id: gen-changelog + run: python .github/resources/changelog-generator.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index 54f4f4fbe8..f08e882aaa 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/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 9840e2d3e4..d01d561988 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -23,6 +23,8 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: πŸ”§ Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - run: git fetch --force --tags - run: echo "Ref name ${{github.ref_name}}" - uses: actions/setup-go@v3 diff --git a/.github/workflows/run-backend-tests.yml b/.github/workflows/run-backend-tests.yml new file mode 100644 index 0000000000..edb58f9a67 --- /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/.gitignore b/.gitignore index 07322c82f4..af3d457e05 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,5 @@ yarn-error.log* .vscode/* frontend-build + +*.tgz diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1c5db8f008..8f608c40cd 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -190,10 +190,34 @@ dockers: - dockerfile: docker/alpine goos: linux goarch: amd64 + use: buildx ids: - all-other-builds image_templates: - - "infisical/cli:{{ .Version }}" - - "infisical/cli:{{ .Major }}.{{ .Minor }}" - - "infisical/cli:{{ .Major }}" - - "infisical/cli:latest" + - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64" + - "infisical/cli:latest-amd64" + build_flag_templates: + - "--pull" + - "--platform=linux/amd64" + - dockerfile: docker/alpine + goos: linux + goarch: amd64 + use: buildx + ids: + - all-other-builds + image_templates: + - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64" + - "infisical/cli:latest-arm64" + build_flag_templates: + - "--pull" + - "--platform=linux/arm64" + +docker_manifests: + - name_template: "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}" + image_templates: + - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64" + - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64" + - name_template: "infisical/cli:latest" + image_templates: + - "infisical/cli:latest-amd64" + - "infisical/cli:latest-arm64" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e735b10d6c..b2a9cabfc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,6 @@ Thanks for taking the time to contribute! πŸ˜ƒ πŸš€ -Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/overview) for instructions on how to contribute. +Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/getting-started/overview) for instructions on how to contribute. We also have some πŸ”₯amazingπŸ”₯ merch for our contributors. Please reach out to tony@infisical.com for more info πŸ‘€ diff --git a/Makefile b/Makefile index 2b7f43c852..11143162ee 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,11 @@ push: up-dev: docker compose -f docker-compose.dev.yml up --build +up-dev-ldap: + docker compose -f docker-compose.dev.yml --profile ldap up --build + up-prod: docker-compose -f docker-compose.prod.yml up --build down: - docker-compose down + docker compose -f docker-compose.dev.yml down diff --git a/backend/.eslintignore b/backend/.eslintignore index c767a4a9eb..660e6d10f8 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 e99c48c097..9c558919b4 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/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts new file mode 100644 index 0000000000..c85244129f --- /dev/null +++ b/backend/e2e-test/mocks/keystore.ts @@ -0,0 +1,30 @@ +import { TKeyStoreFactory } from "@app/keystore/keystore"; + +export const mockKeyStore = (): TKeyStoreFactory => { + const store: Record = {}; + + return { + setItem: async (key, value) => { + store[key] = value; + return "OK"; + }, + setItemWithExpiry: async (key, value) => { + store[key] = value; + return "OK"; + }, + deleteItem: async (key) => { + delete store[key]; + return 1; + }, + getItem: async (key) => { + const value = store[key]; + if (typeof value === "string") { + return value; + } + return null; + }, + incrementBy: async () => { + return 1; + } + }; +}; 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 0000000000..ccb530c796 --- /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 c95e1e0168..cd6ec31947 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 936cfa8590..ec06d64748 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 bc290fed35..4d4bd7ab4c 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 f42c033c2d..ba37b5f420 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 0000000000..a07eda4b9b --- /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 e69de29bb2..03e1c2f503 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 1e424401ed..c1c750225e 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -1,26 +1,31 @@ -// 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"; +import { mockKeyStore } from "./mocks/keystore"; + +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,9 +42,8 @@ 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 }); + const keyStore = mockKeyStore(); + const server = await main({ db, smtp, logger, queue, keyStore }); // @ts-expect-error type globalThis.testServer = server; // @ts-expect-error type @@ -54,6 +58,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 1bce540c65..97eaf4ff8d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,18 +9,19 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.485.0", + "@aws-sdk/client-iam": "^3.525.0", + "@aws-sdk/client-secrets-manager": "^3.504.0", "@casl/ability": "^6.5.0", - "@fastify/cookie": "^9.2.0", - "@fastify/cors": "^8.4.1", + "@fastify/cookie": "^9.3.1", + "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", "@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 +30,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.1553.0", + "axios": "^1.6.7", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", - "bullmq": "^5.1.1", - "dotenv": "^16.3.1", - "fastify": "^4.24.3", + "bullmq": "^5.3.3", + "dotenv": "^16.4.1", + "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", "handlebars": "^4.7.8", "ioredis": "^5.3.2", @@ -45,24 +46,27 @@ "knex": "^3.0.1", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", - "mysql2": "^3.6.5", + "ms": "^2.1.3", + "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", + "passport-ldapauth": "^3.0.1", "pg": "^8.11.3", + "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", - "posthog-node": "^3.6.0", - "probot": "^12.3.3", + "posthog-node": "^3.6.2", + "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 +105,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 +359,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", @@ -675,50 +663,540 @@ "uuid": "dist/bin/uuid" } }, - "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==", + "node_modules/@aws-sdk/client-iam": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.525.0.tgz", + "integrity": "sha512-h705ebOYcgZWN9a4Pdkwd7DAPK4KTEGEFtXEj2FoaadBYBcR9r5yffm7umHZv9gOCHxFMaap8/NSZXirF2VHKg==", "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.525.0", + "@aws-sdk/core": "3.525.0", + "@aws-sdk/credential-provider-node": "3.525.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.525.0", + "@aws-sdk/region-config-resolver": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.525.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.525.0", + "@smithy/config-resolver": "^2.1.4", + "@smithy/core": "^1.3.5", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-retry": "^2.1.4", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^2.2.3", + "@smithy/util-endpoints": "^1.1.4", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "@smithy/util-utf8": "^2.1.1", + "@smithy/util-waiter": "^2.1.3", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.525.0.tgz", + "integrity": "sha512-6KwGQWFoNLH1UupdWPFdKPfTgjSz1kN8/r8aCzuvvXBe4Pz+iDUZ6FEJzGWNc9AapjvZDNO1hs23slomM9rTaA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.525.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.525.0", + "@aws-sdk/region-config-resolver": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.525.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.525.0", + "@smithy/config-resolver": "^2.1.4", + "@smithy/core": "^1.3.5", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-retry": "^2.1.4", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^2.2.3", + "@smithy/util-endpoints": "^1.1.4", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.525.0.tgz", + "integrity": "sha512-zz13k/6RkjPSLmReSeGxd8wzGiiZa4Odr2Tv3wTcxClM4wOjD+zOgGv4Fe32b9AMqaueiCdjbvdu7AKcYxFA4A==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.525.0", + "@aws-sdk/core": "3.525.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.525.0", + "@aws-sdk/region-config-resolver": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.525.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.525.0", + "@smithy/config-resolver": "^2.1.4", + "@smithy/core": "^1.3.5", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-retry": "^2.1.4", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^2.2.3", + "@smithy/util-endpoints": "^1.1.4", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "@smithy/util-utf8": "^2.1.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.525.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sts": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.525.0.tgz", + "integrity": "sha512-a8NUGRvO6rkfTZCbMaCsjDjLbERCwIUU9dIywFYcRgbFhkupJ7fSaZz3Het98U51M9ZbTEpaTa3fz0HaJv8VJw==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.525.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.525.0", + "@aws-sdk/region-config-resolver": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.525.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.525.0", + "@smithy/config-resolver": "^2.1.4", + "@smithy/core": "^1.3.5", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-retry": "^2.1.4", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@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.4", + "@smithy/util-defaults-mode-node": "^2.2.3", + "@smithy/util-endpoints": "^1.1.4", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", + "@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": "^3.525.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.525.0.tgz", + "integrity": "sha512-E3LtEtMWCriQOFZpVKpLYzbdw/v2PAOEAMhn2VRRZ1g0/g1TXzQrfhEU2yd8l/vQEJaCJ82ooGGg7YECviBUxA==", + "dependencies": { + "@smithy/core": "^1.3.5", + "@smithy/protocol-http": "^3.2.1", + "@smithy/signature-v4": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.523.0.tgz", + "integrity": "sha512-Y6DWdH6/OuMDoNKVzZlNeBc6f1Yjk1lYMjANKpIhMbkRCvLJw/PYZKOZa8WpXbTYdgg9XLjKybnLIb3ww3uuzA==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.525.0.tgz", + "integrity": "sha512-RNWQGuSBQZhl3iqklOslUEfQ4br1V3DCPboMpeqFtddUWJV3m2u2extFur9/4Uy+1EHVF120IwZUKtd8dF+ibw==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.525.0.tgz", + "integrity": "sha512-JDnccfK5JRb9jcgpc9lirL9PyCwGIqY0nKdw3LlX5WL5vTpTG4E1q7rLAlpNh7/tFD1n66Itarfv2tsyHMIqCw==", + "dependencies": { + "@aws-sdk/client-sts": "3.525.0", + "@aws-sdk/credential-provider-env": "3.523.0", + "@aws-sdk/credential-provider-process": "3.523.0", + "@aws-sdk/credential-provider-sso": "3.525.0", + "@aws-sdk/credential-provider-web-identity": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@smithy/credential-provider-imds": "^2.2.3", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.525.0.tgz", + "integrity": "sha512-RJXlO8goGXpnoHQAyrCcJ0QtWEOFa34LSbfdqBIjQX/fwnjUuEmiGdXTV3AZmwYQ7juk49tfBneHbtOP3AGqsQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.523.0", + "@aws-sdk/credential-provider-http": "3.525.0", + "@aws-sdk/credential-provider-ini": "3.525.0", + "@aws-sdk/credential-provider-process": "3.523.0", + "@aws-sdk/credential-provider-sso": "3.525.0", + "@aws-sdk/credential-provider-web-identity": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@smithy/credential-provider-imds": "^2.2.3", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.523.0.tgz", + "integrity": "sha512-f0LP9KlFmMvPWdKeUKYlZ6FkQAECUeZMmISsv6NKtvPCI9e4O4cLTeR09telwDK8P0HrgcRuZfXM7E30m8re0Q==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.525.0.tgz", + "integrity": "sha512-7V7ybtufxdD3plxeIeB6aqHZeFIUlAyPphXIUgXrGY10iNcosL970rQPBeggsohe4gCM6UvY2TfMeEcr+ZE8FA==", + "dependencies": { + "@aws-sdk/client-sso": "3.525.0", + "@aws-sdk/token-providers": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.525.0.tgz", + "integrity": "sha512-sAukOjR1oKb2JXG4nPpuBFpSwGUhrrY17PG/xbTy8NAoLLhrqRwnErcLfdTfmj6tH+3094k6ws/Sh8a35ae7fA==", + "dependencies": { + "@aws-sdk/client-sts": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.523.0.tgz", + "integrity": "sha512-4g3q7Ta9sdD9TMUuohBAkbx/e3I/juTqfKi7TPgP+8jxcYX72MOsgemAMHuP6CX27eyj4dpvjH+w4SIVDiDSmg==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-logger": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.523.0.tgz", + "integrity": "sha512-PeDNJNhfiaZx54LBaLTXzUaJ9LXFwDFFIksipjqjvxMafnoVcQwKbkoPUWLe5ytT4nnL1LogD3s55mERFUsnwg==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.523.0.tgz", + "integrity": "sha512-nZ3Vt7ehfSDYnrcg/aAfjjvpdE+61B3Zk68i6/hSUIegT3IH9H1vSW67NDKVp+50hcEfzWwM2HMPXxlzuyFyrw==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.525.0.tgz", + "integrity": "sha512-4al/6uO+t/QIYXK2OgqzDKQzzLAYJza1vWFS+S0lJ3jLNGyLB5BMU5KqWjDzevYZ4eCnz2Nn7z0FveUTNz8YdQ==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.525.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.525.0.tgz", + "integrity": "sha512-8kFqXk6UyKgTMi7N7QlhA6qM4pGPWbiUXqEY2RgUWngtxqNFGeM9JTexZeuavQI+qLLe09VPShPNX71fEDcM6w==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", + "@smithy/util-config-provider": "^2.2.1", + "@smithy/util-middleware": "^2.1.3", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.525.0.tgz", + "integrity": "sha512-puVjbxuK0Dq7PTQ2HdddHy2eQjOH8GZbump74yWJa6JVpRW84LlOcNmP+79x4Kscvz2ldWB8XDFw/pcCiSDe5A==", + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.525.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/types": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", + "dependencies": { + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.525.0.tgz", + "integrity": "sha512-DIW7WWU5tIGkeeKX6NJUyrEIdWMiqjLQG3XBzaUj+ufIENwNjdAHhlD8l2vX7Yr3JZRT6yN/84wBCj7Tw1xd1g==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "@smithy/util-endpoints": "^1.1.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.523.0.tgz", + "integrity": "sha512-6ZRNdGHX6+HQFqTbIA5+i8RWzxFyxsZv8D3soRfpdyWIKkzhSz8IyRKXRciwKBJDaC7OX2jzGE90wxRQft27nA==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.525.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.525.0.tgz", + "integrity": "sha512-88Wjt4efyUSBGcyIuh1dvoMqY1k15jpJc5A/3yi67clBQEFsu9QCodQCQPqmRjV3VRcMtBOk+jeCTiUzTY5dRQ==", + "dependencies": { + "@aws-sdk/types": "3.523.0", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.504.0.tgz", + "integrity": "sha512-JPwsYfQMjs5t74JmA4r1AjpiOG/LEw74d4a8vEdSy3pe2lhl/sSsxSdQtbI30wlJJramngtLNZjxn2+BGDphbg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.504.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/credential-provider-node": "3.504.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 +1213,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.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.504.0.tgz", + "integrity": "sha512-ODA33/nm2srhV08EW0KZAP577UgV0qjyr7Xp2yEo8MXWL4ZqQZprk1c+QKBhjr4Djesrm0VPmSD/np0mtYP68A==", "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.504.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": "^3.504.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", + "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", + "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": "^3.504.0" } }, "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 +1380,32 @@ } }, "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": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.503.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.503.1.tgz", + "integrity": "sha512-rTdlFFGoPPFMF2YjtlfRuSgKI+XsF49u7d98255hySwhsbwd3Xp+utTTPquxP+CwDxMHbDlI7NxDzFiFdsoZug==", + "dependencies": { + "@aws-sdk/types": "3.502.0", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/property-provider": "^2.1.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/util-stream": "^2.1.1", "tslib": "^2.5.0" }, "engines": { @@ -862,19 +1413,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.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.504.0.tgz", + "integrity": "sha512-ODICLXfr8xTUd3wweprH32Ge41yuBa+u3j0JUcLdTUO1N9ldczSMdo8zOPlP0z4doqD3xbnqMkjNQWgN/Q+5oQ==", "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.504.0", + "@aws-sdk/credential-provider-env": "3.502.0", + "@aws-sdk/credential-provider-process": "3.502.0", + "@aws-sdk/credential-provider-sso": "3.504.0", + "@aws-sdk/credential-provider-web-identity": "3.504.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 +1434,21 @@ } }, "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.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.504.0.tgz", + "integrity": "sha512-6+V5hIh+tILmUjf2ZQWQINR3atxQVgH/bFrGdSR/sHSp/tEgw3m0xWL3IRslWU1e4/GtXrfg1iYnMknXy68Ikw==", "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-http": "3.503.1", + "@aws-sdk/credential-provider-ini": "3.504.0", + "@aws-sdk/credential-provider-process": "3.502.0", + "@aws-sdk/credential-provider-sso": "3.504.0", + "@aws-sdk/credential-provider-web-identity": "3.504.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 +1456,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 +1471,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.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.504.0.tgz", + "integrity": "sha512-4MgH2or2SjPzaxM08DCW+BjaX4DSsEGJlicHKmz6fh+w9JmLh750oXcTnbvgUeVz075jcs6qTKjvUcsdGM/t8Q==", "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.504.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 +1488,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.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.504.0.tgz", + "integrity": "sha512-L1ljCvGpIEFdJk087ijf2ohg7HBclOeB1UgBxUBBzf4iPRZTQzd2chGaKj0hm2VVaXz7nglswJeURH5PFcS5oA==", "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/types": "^2.8.0", + "@aws-sdk/client-sts": "3.504.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 +1503,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 +1517,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 +1530,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 +1544,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 +1561,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 +1576,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 +1592,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.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.504.0.tgz", + "integrity": "sha512-YIJWWsZi2ClUiILS1uh5L6VjmCUSTI6KKMuL9DkGjYqJ0aI6M8bd8fT9Wm7QmXCyjcArTgr/Atkhia4T7oKvzQ==", "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.504.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 +1608,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 +1620,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 +1645,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": { @@ -1655,21 +2180,21 @@ } }, "node_modules/@fastify/cookie": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.2.0.tgz", - "integrity": "sha512-fkg1yjjQRHPFAxSHeLC8CqYuNzvR6Lwlj/KjrzQcGjNBK+K82nW+UfCjfN71g1GkoVoc1GTOgIWkFJpcMfMkHQ==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz", + "integrity": "sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==", "dependencies": { "cookie-signature": "^1.1.0", "fastify-plugin": "^4.0.0" } }, "node_modules/@fastify/cors": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.4.1.tgz", - "integrity": "sha512-iYQJtrY3pFiDS5mo5zRaudzg2OcUdJ96PD6xfkKOOEilly5nnrFZx/W6Sce2T79xxlEn2qpU3t5+qS2phS369w==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz", + "integrity": "sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==", "dependencies": { "fastify-plugin": "^4.0.0", - "mnemonist": "0.39.5" + "mnemonist": "0.39.6" } }, "node_modules/@fastify/deepmerge": { @@ -1778,9 +2303,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 +2315,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", @@ -2161,7 +2686,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2174,7 +2698,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -2183,7 +2706,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2193,297 +2715,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 +2797,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 +2851,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 +2879,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 +2930,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 +3010,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 +3077,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": { @@ -3327,11 +3522,11 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.1.tgz", - "integrity": "sha512-1+qdrUqLhaALYL0iOcN43EP6yAXXQ2wWZ6taf4S2pNGowmOc5gx+iMQv+E42JizNJjB0+gEadOXeV1Bf7JWL1Q==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", + "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3339,14 +3534,14 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.1.tgz", - "integrity": "sha512-lxfLDpZm+AWAHPFZps5JfDoO9Ux1764fOgvRUBpHIO8HWHcSN1dkgsago1qLRVgm1BZ8RCm8cgv99QvtaOWIhw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.4.tgz", + "integrity": "sha512-AW2WUZmBAzgO3V3ovKtsUbI3aBNMeQKFDumoqkNxaVDWF/xfnxAWqBKDr/NuG7c06N2Rm4xeZLPiJH/d+na0HA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/types": "^2.9.1", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", "@smithy/util-config-provider": "^2.2.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -3354,17 +3549,17 @@ } }, "node_modules/@smithy/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.1.tgz", - "integrity": "sha512-tf+NIu9FkOh312b6M9G4D68is4Xr7qptzaZGZUREELF8ysE1yLKphqt7nsomjKZVwW7WE5pDDex9idowNGRQ/Q==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.5.tgz", + "integrity": "sha512-Rrc+e2Jj6Gu7Xbn0jvrzZlSiP2CZocIOfZ9aNUA82+1sa6GBnxqL9+iZ9EKHeD9aqD1nU8EK4+oN2EiFpSv7Yw==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-retry": "^2.1.4", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -3372,14 +3567,14 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.1.tgz", - "integrity": "sha512-7XHjZUxmZYnONheVQL7j5zvZXga+EWNgwEAP6OPZTi7l8J4JTeNh9aIOfE5fKHZ/ee2IeNOh54ZrSna+Vc6TFA==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.4.tgz", + "integrity": "sha512-DdatjmBZQnhGe1FhI8gO98f7NmvQFSDiZTwC3WMvLTCKQUY+Y1SVkhJqIuLu50Eb7pTheoXQmK+hKYUgpUWsNA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -3387,12 +3582,12 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.1.tgz", - "integrity": "sha512-E8KYBxBIuU4c+zrpR22VsVrOPoEDzk35bQR3E+xm4k6Pa6JqzkDOdMyf9Atac5GPNKHJBdVaQ4JtjdWX2rl/nw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.3.tgz", + "integrity": "sha512-rGlCVuwSDv6qfKH4/lRxFjcZQnIE0LZ3D4lkMHg7ZSltK9rA74r0VuGSvWVQ4N/d70VZPaniFhp4Z14QYZsa+A==", "dependencies": { "@aws-crypto/crc32": "3.0.0", - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-hex-encoding": "^2.1.1", "tslib": "^2.5.0" } @@ -3449,23 +3644,23 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.1.tgz", - "integrity": "sha512-VYGLinPsFqH68lxfRhjQaSkjXM7JysUOJDTNjHBuN/ykyRb2f1gyavN9+VhhPTWCy32L4yZ2fdhpCs/nStEicg==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", + "integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", "dependencies": { - "@smithy/protocol-http": "^3.1.1", - "@smithy/querystring-builder": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", "@smithy/util-base64": "^2.1.1", "tslib": "^2.5.0" } }, "node_modules/@smithy/hash-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.1.tgz", - "integrity": "sha512-Qhoq0N8f2OtCnvUpCf+g1vSyhYQrZjhSwvJ9qvR8BUGOtTXiyv2x1OD2e6jVGmlpC4E4ax1USHoyGfV9JFsACg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.3.tgz", + "integrity": "sha512-FsAPCUj7VNJIdHbSxMd5uiZiF20G2zdSDgrgrDrHqIs/VMxK85Vqk5kMVNNDMCZmMezp6UKnac0B4nAyx7HJ9g==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-buffer-from": "^2.1.1", "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" @@ -3475,11 +3670,11 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.1.tgz", - "integrity": "sha512-7WTgnKw+VPg8fxu2v9AlNOQ5yaz6RA54zOVB4f6vQuR0xFKd+RzlCpt0WidYTsye7F+FYDIaS/RnJW4pxjNInw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.3.tgz", + "integrity": "sha512-wkra7d/G4CbngV4xsjYyAYOvdAhahQje/WymuQdVEnXFExJopEu7fbL5AEAlBPgWHXwu94VnCSG00gVzRfExyg==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" } }, @@ -3495,12 +3690,12 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.1.tgz", - "integrity": "sha512-rSr9ezUl9qMgiJR0UVtVOGEZElMdGFyl8FzWEF5iEKTlcWxGr2wTqGfDwtH3LAB7h+FPkxqv4ZU4cpuCN9Kf/g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.3.tgz", + "integrity": "sha512-aJduhkC+dcXxdnv5ZpM3uMmtGmVFKx412R1gbeykS5HXDmRU6oSsyy2SoHENCkfOGKAQOjVE2WVqDJibC0d21g==", "dependencies": { - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3508,16 +3703,16 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.1.tgz", - "integrity": "sha512-XPZTb1E2Oav60Ven3n2PFx+rX9EDsU/jSTA8VDamt7FXks67ekjPY/XrmmPDQaFJOTUHJNKjd8+kZxVO5Ael4Q==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", + "integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", "dependencies": { - "@smithy/middleware-serde": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -3525,17 +3720,17 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.1.tgz", - "integrity": "sha512-eMIHOBTXro6JZ+WWzZWd/8fS8ht5nS5KDQjzhNMHNRcG5FkNTqcKpYhw7TETMYzbLfhO5FYghHy1vqDWM4FLDA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.4.tgz", + "integrity": "sha512-Cyolv9YckZTPli1EkkaS39UklonxMd08VskiuMhURDjC0HHa/AD6aK/YoD21CHv9s0QLg0WMLvk9YeLTKkXaFQ==", "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/service-error-classification": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/protocol-http": "^3.2.1", + "@smithy/service-error-classification": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", "tslib": "^2.5.0", "uuid": "^8.3.2" }, @@ -3552,11 +3747,11 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.1.tgz", - "integrity": "sha512-D8Gq0aQBeE1pxf3cjWVkRr2W54t+cdM2zx78tNrVhqrDykRA7asq8yVJij1u5NDtKzKqzBSPYh7iW0svUKg76g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", + "integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3564,11 +3759,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.1.tgz", - "integrity": "sha512-KPJhRlhsl8CjgGXK/DoDcrFGfAqoqvuwlbxy+uOO4g2Azn1dhH+GVfC3RAp+6PoL5PWPb+vt6Z23FP+Mr6qeCw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", + "integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3576,13 +3771,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.1.tgz", - "integrity": "sha512-epzK3x1xNxA9oJgHQ5nz+2j6DsJKdHfieb+YgJ7ATWxzNcB7Hc+Uya2TUck5MicOPhDV8HZImND7ZOecVr+OWg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", + "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", "dependencies": { - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3590,14 +3785,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.3.1.tgz", - "integrity": "sha512-gLA8qK2nL9J0Rk/WEZSvgin4AppvuCYRYg61dcUo/uKxvMZsMInL5I5ZdJTogOvdfVug3N2dgI5ffcUfS4S9PA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", + "integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", "dependencies": { - "@smithy/abort-controller": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/querystring-builder": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/abort-controller": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3605,11 +3800,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.1.tgz", - "integrity": "sha512-FX7JhhD/o5HwSwg6GLK9zxrMUrGnb3PzNBrcthqHKBc3dH0UfgEAU24xnJ8F0uow5mj17UeBEOI6o3CF2k7Mhw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", + "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3617,11 +3812,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.1.1.tgz", - "integrity": "sha512-6ZRTSsaXuSL9++qEwH851hJjUA0OgXdQFCs+VDw4tGH256jQ3TjYY/i34N4vd24RV3nrjNsgd1yhb57uMoKbzQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3629,11 +3824,11 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.1.tgz", - "integrity": "sha512-C/ko/CeEa8jdYE4gt6nHO5XDrlSJ3vdCG0ZAc6nD5ZIE7LBp0jCx4qoqp7eoutBu7VrGMXERSRoPqwi1WjCPbg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", + "integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-uri-escape": "^2.1.1", "tslib": "^2.5.0" }, @@ -3642,11 +3837,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.1.tgz", - "integrity": "sha512-H4+6jKGVhG1W4CIxfBaSsbm98lOO88tpDWmZLgkJpt8Zkk/+uG0FmmqMuCAc3HNM2ZDV+JbErxr0l5BcuIf/XQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", + "integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3654,22 +3849,22 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.1.tgz", - "integrity": "sha512-txEdZxPUgM1PwGvDvHzqhXisrc5LlRWYCf2yyHfvITWioAKat7srQvpjMAvgzf0t6t7j8yHrryXU9xt7RZqFpw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.3.tgz", + "integrity": "sha512-iUrpSsem97bbXHHT/v3s7vaq8IIeMo6P6cXdeYHrx0wOJpMeBGQF7CB0mbJSiTm3//iq3L55JiEm8rA7CTVI8A==", "dependencies": { - "@smithy/types": "^2.9.1" + "@smithy/types": "^2.10.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.1.tgz", - "integrity": "sha512-2E2kh24igmIznHLB6H05Na4OgIEilRu0oQpYXo3LCNRrawHAcfDKq9004zJs+sAMt2X5AbY87CUCJ7IpqpSgdw==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", + "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3677,15 +3872,15 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.1.tgz", - "integrity": "sha512-Hb7xub0NHuvvQD3YwDSdanBmYukoEkhqBjqoxo+bSdC0ryV9cTfgmNjuAQhTPYB6yeU7hTR+sPRiFMlxqv6kmg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", + "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", "dependencies": { - "@smithy/eventstream-codec": "^2.1.1", + "@smithy/eventstream-codec": "^2.1.3", "@smithy/is-array-buffer": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-hex-encoding": "^2.1.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", "@smithy/util-uri-escape": "^2.1.1", "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" @@ -3695,15 +3890,15 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.3.1.tgz", - "integrity": "sha512-YsTdU8xVD64r2pLEwmltrNvZV6XIAC50LN6ivDopdt+YiF/jGH6PY9zUOu0CXD/d8GMB8gbhnpPsdrjAXHS9QA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", + "integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", - "@smithy/util-stream": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -3711,9 +3906,9 @@ } }, "node_modules/@smithy/types": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.9.1.tgz", - "integrity": "sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", "dependencies": { "tslib": "^2.5.0" }, @@ -3722,12 +3917,12 @@ } }, "node_modules/@smithy/url-parser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.1.tgz", - "integrity": "sha512-qC9Bv8f/vvFIEkHsiNrUKYNl8uKQnn4BdhXl7VzQRP774AwIjiSMMwkbT+L7Fk8W8rzYVifzJNYxv1HwvfBo3Q==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", + "integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", "dependencies": { - "@smithy/querystring-parser": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/querystring-parser": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" } }, @@ -3786,13 +3981,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.1.tgz", - "integrity": "sha512-lqLz/9aWRO6mosnXkArtRuQqqZBhNpgI65YDpww4rVQBuUT7qzKbDLG5AmnQTCiU4rOquaZO/Kt0J7q9Uic7MA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.4.tgz", + "integrity": "sha512-J6XAVY+/g7jf03QMnvqPyU+8jqGrrtXoKWFVOS+n1sz0Lg8HjHJ1ANqaDN+KTTKZRZlvG8nU5ZrJOUL6VdwgcQ==", "dependencies": { - "@smithy/property-provider": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", "bowser": "^2.11.0", "tslib": "^2.5.0" }, @@ -3801,16 +3996,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.1.1.tgz", - "integrity": "sha512-tYVrc+w+jSBfBd267KDnvSGOh4NMz+wVH7v4CClDbkdPfnjvImBZsOURncT5jsFwR9KCuDyPoSZq4Pa6+eCUrA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.3.tgz", + "integrity": "sha512-ttUISrv1uVOjTlDa3nznX33f0pthoUlP+4grhTvOzcLhzArx8qHB94/untGACOG3nlf8vU20nI2iWImfzoLkYA==", "dependencies": { - "@smithy/config-resolver": "^2.1.1", - "@smithy/credential-provider-imds": "^2.2.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/config-resolver": "^2.1.4", + "@smithy/credential-provider-imds": "^2.2.4", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/property-provider": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3818,12 +4013,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.1.tgz", - "integrity": "sha512-sI4d9rjoaekSGEtq3xSb2nMjHMx8QXcz2cexnVyRWsy4yQ9z3kbDpX+7fN0jnbdOp0b3KSTZJZ2Yb92JWSanLw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.4.tgz", + "integrity": "sha512-/qAeHmK5l4yQ4/bCIJ9p49wDe9rwWtOzhPHblu386fwPNT3pxmodgcs9jDCV52yK9b4rB8o9Sj31P/7Vzka1cg==", "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/types": "^2.9.1", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3842,11 +4037,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.1.tgz", - "integrity": "sha512-mKNrk8oz5zqkNcbcgAAepeJbmfUW6ogrT2Z2gDbIUzVzNAHKJQTYmH9jcy0jbWb+m7ubrvXKb6uMjkSgAqqsFA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", + "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3854,12 +4049,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.1.tgz", - "integrity": "sha512-Mg+xxWPTeSPrthpC5WAamJ6PW4Kbo01Fm7lWM1jmGRvmrRdsd3192Gz2fBXAMURyXpaNxyZf6Hr/nQ4q70oVEA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.3.tgz", + "integrity": "sha512-Kbvd+GEMuozbNUU3B89mb99tbufwREcyx2BOX0X2+qHjq6Gvsah8xSDDgxISDwcOHoDqUWO425F0Uc/QIRhYkg==", "dependencies": { - "@smithy/service-error-classification": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/service-error-classification": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -3867,13 +4062,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.1.tgz", - "integrity": "sha512-J7SMIpUYvU4DQN55KmBtvaMc7NM3CZ2iWICdcgaovtLzseVhAqFRYqloT3mh0esrFw+3VEK6nQFteFsTqZSECQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", + "integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", "dependencies": { - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/types": "^2.10.1", "@smithy/util-base64": "^2.1.1", "@smithy/util-buffer-from": "^2.1.1", "@smithy/util-hex-encoding": "^2.1.1", @@ -3907,6 +4102,19 @@ "node": ">=14.0.0" } }, + "node_modules/@smithy/util-waiter": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.1.3.tgz", + "integrity": "sha512-3R0wNFAQQoH9e4m+bVLDYNOst2qNxtxFgq03WoNHWTBOqQT3jFnOBRj1W51Rf563xDA5kwqjziksxn6RKkHB+Q==", + "dependencies": { + "@smithy/abort-controller": "^2.1.3", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@swc/core": { "version": "1.3.107", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz", @@ -4207,6 +4415,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 +4448,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", @@ -4279,6 +4480,14 @@ "integrity": "sha512-2h3tFvkbHksiNcDiUdcJ08gXWG10fnahp30GJ2Tbt4vd4pfsbfkoKTaTbYykFoppaJ6DL3914nQ3PU1vVIlBRQ==", "dev": true }, + "node_modules/@types/ldapjs": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz", + "integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/libsodium-wrappers": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", @@ -4460,51 +4669,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 +5091,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 +5105,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 +5146,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 +5160,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 +5172,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 +5249,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 +5336,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" }, @@ -5471,6 +5635,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -5536,9 +5716,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.1553.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1553.0.tgz", + "integrity": "sha512-CfZaw8dR9e642aBOeFhkFL7KoQApeLR15uH2IQqfL/12snWYayAAesYh0tEaU+XbhrH0CUsf2Zro5IraEXEZMg==", "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -5549,7 +5729,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 +5776,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.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -5617,6 +5817,17 @@ "axios": "0.x || 1.x" } }, + "node_modules/backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", + "dependencies": { + "precond": "0.2" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5662,6 +5873,11 @@ "node": ">= 10.0.0" } }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -5676,6 +5892,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", @@ -5746,7 +5985,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -5796,21 +6034,44 @@ } }, "node_modules/bullmq": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.1.1.tgz", - "integrity": "sha512-j3zbNEQWsyHjpqGWiem2XBfmxAjYcArbwsmGlkM1E9MAVcrqB5hQUsXmyy9gEBAdL+PVotMICr7xTquR4Y2sKQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.3.3.tgz", + "integrity": "sha512-Gc/68HxiCHLMPBiGIqtINxcf8HER/5wvBYMY/6x3tFejlvldUBFaAErMTLDv4TnPsTyzNPrfBKmFCEM58uVnJg==", "dependencies": { "cron-parser": "^4.6.0", - "glob": "^8.0.3", + "fast-glob": "^3.3.2", "ioredis": "^5.3.2", "lodash": "^4.17.21", - "msgpackr": "^1.6.2", + "minimatch": "^9.0.3", + "msgpackr": "^1.10.1", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^9.0.0" } }, + "node_modules/bullmq/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bullmq/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/bundle-require": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz", @@ -5874,9 +6135,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 +6152,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,12 +6227,29 @@ "node": ">=6" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "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": ">=0.8" + "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/cluster-key-slot": { @@ -6000,7 +6289,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", @@ -6072,6 +6362,11 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -6282,9 +6577,9 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", "engines": { "node": ">=12" }, @@ -6319,8 +6614,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 +6629,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 +7295,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 +7328,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 +7419,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", @@ -7154,6 +7437,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-content-type-parse": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", @@ -7162,7 +7453,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", @@ -7184,7 +7476,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -7248,19 +7539,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 +7561,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 +7582,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 +7598,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", @@ -7334,7 +7627,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7373,9 +7665,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 +7742,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 +7972,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", @@ -7783,7 +8078,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -8026,6 +8320,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 +8330,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", @@ -8382,7 +8678,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8413,7 +8708,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -8421,6 +8715,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", @@ -8437,7 +8742,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -8466,14 +8770,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 +8870,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", @@ -8876,6 +9183,57 @@ "node": ">=8" } }, + "node_modules/ldap-filter": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", + "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ldapauth-fork": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.5.tgz", + "integrity": "sha512-LWUk76+V4AOZbny/3HIPQtGPWZyA3SW2tRhsWIBi9imP22WJktKLHV1ofd8Jo/wY7Ve6vAT7FCI5mEn3blZTjw==", + "dependencies": { + "@types/ldapjs": "^2.2.2", + "bcryptjs": "^2.4.0", + "ldapjs": "^2.2.1", + "lru-cache": "^7.10.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ldapauth-fork/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/ldapjs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", + "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", + "dependencies": { + "abstract-logging": "^2.0.0", + "asn1": "^0.2.4", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "ldap-filter": "^0.3.3", + "once": "^1.4.0", + "vasync": "^2.2.0", + "verror": "^1.8.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -9008,11 +9366,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 +9423,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 +9477,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" @@ -9181,7 +9549,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -9198,7 +9565,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -9211,7 +9577,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -9253,7 +9618,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" } @@ -9332,9 +9696,9 @@ } }, "node_modules/mnemonist": { - "version": "0.39.5", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.5.tgz", - "integrity": "sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==", + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", "dependencies": { "obliterator": "^2.0.1" } @@ -9395,9 +9759,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", @@ -9496,17 +9860,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node_modules/node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -9777,38 +10130,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 +10177,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 +10209,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 +10260,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", @@ -10018,6 +10366,18 @@ "node": ">= 0.4.0" } }, + "node_modules/passport-ldapauth": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz", + "integrity": "sha512-TRRx3BHi8GC8MfCT9wmghjde/EGeKjll7zqHRRfGRxXbLcaDce2OftbQrFG7/AWaeFhR6zpZHtBQ/IkINdLVjQ==", + "dependencies": { + "ldapauth-fork": "^5.0.1", + "passport-strategy": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", @@ -10174,6 +10534,14 @@ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, + "node_modules/pg-cursor": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.10.3.tgz", + "integrity": "sha512-rDyBVoqPVnx/PTmnwQAYgusSeAKlTL++gmpf5klVK+mYMFEqsOc6VHHZnPKc/4lOvr4r6fiMuoxSFuBF1dx4FQ==", + "peerDependencies": { + "pg": "^8" + } + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -10204,6 +10572,17 @@ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" }, + "node_modules/pg-query-stream": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.5.3.tgz", + "integrity": "sha512-ufa94r/lHJdjAm3+zPZEO0gXAmCb4tZPaOt7O76mjcxdL/HxwTuryy76km+u0odBBgtfdKFYq/9XGfiYeQF0yA==", + "dependencies": { + "pg-cursor": "^2.10.3" + }, + "peerDependencies": { + "pg": "^8" + } + }, "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", @@ -10258,16 +10637,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 +10667,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 +10712,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", @@ -10590,9 +10940,9 @@ "dev": true }, "node_modules/posthog-node": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.6.0.tgz", - "integrity": "sha512-N/4//SIQR4fhwbHnDdJ2rQCYdu9wo0EVPK4lVgZswp5R/E42RKlpuO6ZfPsBl+Bcg06OYiOd/WR/jLV90FCoSw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.6.2.tgz", + "integrity": "sha512-tVIaShR3SxBx17AlAUS86jQTweKuJIFRedBB504fCz7YPnXJTYSrVcUHn5IINE2wu4jUQimQK6ihQr90Djrdrg==", "dependencies": { "axios": "^1.6.2", "rusha": "^0.8.14" @@ -10601,6 +10951,14 @@ "node": ">=15.0.0" } }, + "node_modules/precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10665,413 +11023,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": { @@ -11193,7 +11197,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -11319,11 +11322,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 +11400,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", @@ -11530,7 +11543,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11891,11 +11903,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 +11936,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 +11963,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 +12070,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 +12322,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" @@ -12307,7 +12343,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -13374,6 +13409,43 @@ "node": ">= 0.8" } }, + "node_modules/vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "verror": "1.10.0" + } + }, + "node_modules/vasync/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vite": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", @@ -13430,9 +13502,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 +13978,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 +14000,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 +14521,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 6b4d32317e..5c22dc2377 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,21 +67,22 @@ "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-iam": "^3.525.0", + "@aws-sdk/client-secrets-manager": "^3.504.0", "@casl/ability": "^6.5.0", - "@fastify/cookie": "^9.2.0", - "@fastify/cors": "^8.4.1", + "@fastify/cookie": "^9.3.1", + "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", "@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 +91,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.1553.0", + "axios": "^1.6.7", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", - "bullmq": "^5.1.1", - "dotenv": "^16.3.1", - "fastify": "^4.24.3", + "bullmq": "^5.3.3", + "dotenv": "^16.4.1", + "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", "handlebars": "^4.7.8", "ioredis": "^5.3.2", @@ -106,23 +107,26 @@ "knex": "^3.0.1", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", - "mysql2": "^3.6.5", + "ms": "^2.1.3", + "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", + "passport-ldapauth": "^3.0.1", "pg": "^8.11.3", + "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", - "posthog-node": "^3.6.0", - "probot": "^12.3.3", + "posthog-node": "^3.6.2", + "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 25faf94c30..c79ea0846f 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/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 68330613ca..8c913991fd 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -44,7 +44,7 @@ const getZodDefaultValue = (type: unknown, value: string | number | boolean | Ob if (!value || value === "null") return; switch (type) { case "uuid": - return; + return `.default("00000000-0000-0000-0000-000000000000")`; case "character varying": { if (value === "gen_random_uuid()") return; if (typeof value === "string" && value.includes("::")) { @@ -100,7 +100,8 @@ const main = async () => { const columnName = columnNames[colNum]; const colInfo = columns[columnName]; let ztype = getZodPrimitiveType(colInfo.type); - if (colInfo.defaultValue) { + // don't put optional on id + if (colInfo.defaultValue && columnName !== "id") { const { defaultValue } = colInfo; const zSchema = getZodDefaultValue(colInfo.type, defaultValue); if (zSchema) { @@ -120,6 +121,7 @@ const main = async () => { .split("_") .reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, ""); + // the insert and update are changed to zod input type to use default cases writeFileSync( path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`), `// Code generated by automation script, DO NOT EDIT. @@ -134,8 +136,8 @@ import { TImmutableDBKeys } from "./models"; export const ${pascalCase}Schema = z.object({${schema}}); export type T${pascalCase} = z.infer; -export type T${pascalCase}Insert = Omit; -export type T${pascalCase}Update = Partial>; +export type T${pascalCase}Insert = Omit, TImmutableDBKeys>; +export type T${pascalCase}Update = Partial, TImmutableDBKeys>>; ` ); } diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index ef850bb8c6..649c54c547 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -3,9 +3,11 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; +import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; 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"; @@ -68,6 +70,7 @@ declare module "fastify" { }; auditLogInfo: Pick; ssoConfig: Awaited>; + ldapConfig: Awaited>; } interface FastifyInstance { @@ -105,6 +108,8 @@ declare module "fastify" { secretRotation: TSecretRotationServiceFactory; snapshot: TSecretSnapshotServiceFactory; saml: TSamlConfigServiceFactory; + scim: TScimServiceFactory; + ldap: TLdapConfigServiceFactory; auditLog: TAuditLogServiceFactory; secretScanning: TSecretScanningServiceFactory; license: TLicenseServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 5bf5638104..31b80e0e33 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -32,6 +32,9 @@ import { TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate, + TIdentityProjectMembershipRole, + TIdentityProjectMembershipRoleInsert, + TIdentityProjectMembershipRoleUpdate, TIdentityProjectMemberships, TIdentityProjectMembershipsInsert, TIdentityProjectMembershipsUpdate, @@ -50,6 +53,9 @@ import { TIntegrations, TIntegrationsInsert, TIntegrationsUpdate, + TLdapConfigs, + TLdapConfigsInsert, + TLdapConfigsUpdate, TOrganizations, TOrganizationsInsert, TOrganizationsUpdate, @@ -80,9 +86,15 @@ import { TProjects, TProjectsInsert, TProjectsUpdate, + TProjectUserMembershipRoles, + TProjectUserMembershipRolesInsert, + TProjectUserMembershipRolesUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, + TScimTokens, + TScimTokensInsert, + TScimTokensUpdate, TSecretApprovalPolicies, TSecretApprovalPoliciesApprovers, TSecretApprovalPoliciesApproversInsert, @@ -158,6 +170,9 @@ import { TUserActions, TUserActionsInsert, TUserActionsUpdate, + TUserAliases, + TUserAliasesInsert, + TUserAliasesUpdate, TUserEncryptionKeys, TUserEncryptionKeysInsert, TUserEncryptionKeysUpdate, @@ -172,6 +187,7 @@ import { declare module "knex/types/tables" { interface Tables { [TableName.Users]: Knex.CompositeTableType; + [TableName.UserAliases]: Knex.CompositeTableType; [TableName.UserEncryptionKey]: Knex.CompositeTableType< TUserEncryptionKeys, TUserEncryptionKeysInsert, @@ -211,6 +227,11 @@ declare module "knex/types/tables" { TProjectEnvironmentsUpdate >; [TableName.ProjectBot]: Knex.CompositeTableType; + [TableName.ProjectUserMembershipRole]: Knex.CompositeTableType< + TProjectUserMembershipRoles, + TProjectUserMembershipRolesInsert, + TProjectUserMembershipRolesUpdate + >; [TableName.ProjectRoles]: Knex.CompositeTableType; [TableName.ProjectKeys]: Knex.CompositeTableType; [TableName.Secret]: Knex.CompositeTableType; @@ -262,6 +283,12 @@ declare module "knex/types/tables" { TIdentityProjectMembershipsInsert, TIdentityProjectMembershipsUpdate >; + [TableName.IdentityProjectMembershipRole]: Knex.CompositeTableType< + TIdentityProjectMembershipRole, + TIdentityProjectMembershipRoleInsert, + TIdentityProjectMembershipRoleUpdate + >; + [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, TSecretApprovalPoliciesInsert, @@ -314,6 +341,7 @@ declare module "knex/types/tables" { TSecretSnapshotFoldersUpdate >; [TableName.SamlConfig]: Knex.CompositeTableType; + [TableName.LdapConfig]: Knex.CompositeTableType; [TableName.OrgBot]: Knex.CompositeTableType; [TableName.AuditLog]: Knex.CompositeTableType; [TableName.GitAppInstallSession]: Knex.CompositeTableType< diff --git a/backend/src/cache/redis.ts b/backend/src/cache/redis.ts deleted file mode 100644 index 4e856fac15..0000000000 --- a/backend/src/cache/redis.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Redis from "ioredis"; - -export const initRedisConnection = (redisUrl: string) => { - const redis = new Redis(redisUrl); - return redis; -}; diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index 2a321a3bc3..bd4ce99c16 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -6,6 +6,13 @@ export const initDbConnection = ({ dbConnectionUri, dbRootCert }: { dbConnection client: "pg", connection: { connectionString: dbConnectionUri, + host: process.env.DB_HOST, + // @ts-expect-error I have no clue why only for the port there is a type error + // eslint-disable-next-line + port: process.env.DB_PORT, + user: process.env.DB_USER, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, ssl: dbRootCert ? { rejectUnauthorized: true, diff --git a/backend/src/db/knexfile.ts b/backend/src/db/knexfile.ts index 73eb507f40..8af2b59ab2 100644 --- a/backend/src/db/knexfile.ts +++ b/backend/src/db/knexfile.ts @@ -7,13 +7,29 @@ import path from "path"; // Update with your config settings. . dotenv.config({ - path: path.join(__dirname, "../../../.env.migration"), - debug: true + path: path.join(__dirname, "../../../.env.migration") }); +dotenv.config({ + path: path.join(__dirname, "../../../.env") +}); + export default { development: { client: "postgres", - connection: process.env.DB_CONNECTION_URI, + connection: { + connectionString: process.env.DB_CONNECTION_URI, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + user: process.env.DB_USER, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + ssl: process.env.DB_ROOT_CERT + ? { + rejectUnauthorized: true, + ca: Buffer.from(process.env.DB_ROOT_CERT, "base64").toString("ascii") + } + : false + }, pool: { min: 2, max: 10 @@ -27,7 +43,20 @@ export default { }, production: { client: "postgres", - connection: process.env.DB_CONNECTION_URI, + connection: { + connectionString: process.env.DB_CONNECTION_URI, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + user: process.env.DB_USER, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + ssl: process.env.DB_ROOT_CERT + ? { + rejectUnauthorized: true, + ca: Buffer.from(process.env.DB_ROOT_CERT, "base64").toString("ascii") + } + : false + }, pool: { min: 2, max: 10 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 0000000000..28121362a4 --- /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 0000000000..d0a840ee27 --- /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/migrations/20240222201806_admin-signup-control.ts b/backend/src/db/migrations/20240222201806_admin-signup-control.ts new file mode 100644 index 0000000000..c52f753c00 --- /dev/null +++ b/backend/src/db/migrations/20240222201806_admin-signup-control.ts @@ -0,0 +1,20 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const isTablePresent = await knex.schema.hasTable(TableName.SuperAdmin); + if (isTablePresent) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.string("allowedSignUpDomain"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SuperAdmin, "allowedSignUpDomain")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("allowedSignUpDomain"); + }); + } +} diff --git a/backend/src/db/migrations/20240226094411_instance-id.ts b/backend/src/db/migrations/20240226094411_instance-id.ts new file mode 100644 index 0000000000..094defdc88 --- /dev/null +++ b/backend/src/db/migrations/20240226094411_instance-id.ts @@ -0,0 +1,25 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +const ADMIN_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.uuid("instanceId").notNullable().defaultTo(knex.fn.uuid()); + }); + + const superUserConfigExists = await knex(TableName.SuperAdmin).where("id", ADMIN_CONFIG_UUID).first(); + if (!superUserConfigExists) { + // eslint-disable-next-line + await knex(TableName.SuperAdmin).update({ id: ADMIN_CONFIG_UUID }).whereNotNull("id").limit(1); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("instanceId"); + }); +} diff --git a/backend/src/db/migrations/20240307232900_integration-last-used.ts b/backend/src/db/migrations/20240307232900_integration-last-used.ts new file mode 100644 index 0000000000..c64c31881f --- /dev/null +++ b/backend/src/db/migrations/20240307232900_integration-last-used.ts @@ -0,0 +1,15 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.Integration, (t) => { + t.datetime("lastUsed"); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.Integration, (t) => { + t.dropColumn("lastUsed"); + }); +} diff --git a/backend/src/db/migrations/20240311210135_ldap-config.ts b/backend/src/db/migrations/20240311210135_ldap-config.ts new file mode 100644 index 0000000000..93ac2c7ac8 --- /dev/null +++ b/backend/src/db/migrations/20240311210135_ldap-config.ts @@ -0,0 +1,68 @@ +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.LdapConfig))) { + await knex.schema.createTable(TableName.LdapConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.boolean("isActive").notNullable(); + t.string("url").notNullable(); + t.string("encryptedBindDN").notNullable(); + t.string("bindDNIV").notNullable(); + t.string("bindDNTag").notNullable(); + t.string("encryptedBindPass").notNullable(); + t.string("bindPassIV").notNullable(); + t.string("bindPassTag").notNullable(); + t.string("searchBase").notNullable(); + t.text("encryptedCACert").notNullable(); + t.string("caCertIV").notNullable(); + t.string("caCertTag").notNullable(); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.LdapConfig); + + if (!(await knex.schema.hasTable(TableName.UserAliases))) { + await knex.schema.createTable(TableName.UserAliases, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("userId").notNullable(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.string("username").notNullable(); + t.string("aliasType").notNullable(); + t.string("externalId").notNullable(); + t.specificType("emails", "text[]"); + t.uuid("orgId").nullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.UserAliases); + + await knex.schema.alterTable(TableName.Users, (t) => { + t.string("username").unique(); + t.string("email").nullable().alter(); + t.dropUnique(["email"]); + }); + + await knex(TableName.Users).update("username", knex.ref("email")); + + await knex.schema.alterTable(TableName.Users, (t) => { + t.string("username").notNullable().alter(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.LdapConfig); + await knex.schema.dropTableIfExists(TableName.UserAliases); + await knex.schema.alterTable(TableName.Users, (t) => { + t.dropColumn("username"); + // t.string("email").notNullable().alter(); + }); + await dropOnUpdateTrigger(knex, TableName.LdapConfig); +} diff --git a/backend/src/db/migrations/20240312162549_temp-roles.ts b/backend/src/db/migrations/20240312162549_temp-roles.ts new file mode 100644 index 0000000000..ec78821fe9 --- /dev/null +++ b/backend/src/db/migrations/20240312162549_temp-roles.ts @@ -0,0 +1,50 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const doesTableExist = await knex.schema.hasTable(TableName.ProjectUserMembershipRole); + if (!doesTableExist) { + await knex.schema.createTable(TableName.ProjectUserMembershipRole, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("role").notNullable(); + t.uuid("projectMembershipId").notNullable(); + t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + // until role is changed/removed the role should not deleted + t.uuid("customRoleId"); + t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.ProjectUserMembershipRole); + + const projectMemberships = await knex(TableName.ProjectMembership).select( + "id", + "role", + "createdAt", + "updatedAt", + knex.ref("roleId").withSchema(TableName.ProjectMembership).as("customRoleId") + ); + if (projectMemberships.length) + await knex.batchInsert( + TableName.ProjectUserMembershipRole, + projectMemberships.map((data) => ({ ...data, projectMembershipId: data.id })) + ); + // will be dropped later + // await knex.schema.alterTable(TableName.ProjectMembership, (t) => { + // t.dropColumn("roleId"); + // t.dropColumn("role"); + // }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ProjectUserMembershipRole); + await dropOnUpdateTrigger(knex, TableName.ProjectUserMembershipRole); +} diff --git a/backend/src/db/migrations/20240312162556_temp-role-identity.ts b/backend/src/db/migrations/20240312162556_temp-role-identity.ts new file mode 100644 index 0000000000..dbd188d899 --- /dev/null +++ b/backend/src/db/migrations/20240312162556_temp-role-identity.ts @@ -0,0 +1,52 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + const doesTableExist = await knex.schema.hasTable(TableName.IdentityProjectMembershipRole); + if (!doesTableExist) { + await knex.schema.createTable(TableName.IdentityProjectMembershipRole, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("role").notNullable(); + t.uuid("projectMembershipId").notNullable(); + t.foreign("projectMembershipId") + .references("id") + .inTable(TableName.IdentityProjectMembership) + .onDelete("CASCADE"); + // until role is changed/removed the role should not deleted + t.uuid("customRoleId"); + t.foreign("customRoleId").references("id").inTable(TableName.ProjectRoles); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityProjectMembershipRole); + + const identityMemberships = await knex(TableName.IdentityProjectMembership).select( + "id", + "role", + "createdAt", + "updatedAt", + knex.ref("roleId").withSchema(TableName.IdentityProjectMembership).as("customRoleId") + ); + if (identityMemberships.length) + await knex.batchInsert( + TableName.IdentityProjectMembershipRole, + identityMemberships.map((data) => ({ ...data, projectMembershipId: data.id })) + ); + // await knex.schema.alterTable(TableName.IdentityProjectMembership, (t) => { + // t.dropColumn("roleId"); + // t.dropColumn("role"); + // }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityProjectMembershipRole); + await dropOnUpdateTrigger(knex, TableName.IdentityProjectMembershipRole); +} diff --git a/backend/src/db/schemas/api-keys.ts b/backend/src/db/schemas/api-keys.ts index ff29a54e10..cf836fd888 100644 --- a/backend/src/db/schemas/api-keys.ts +++ b/backend/src/db/schemas/api-keys.ts @@ -19,5 +19,5 @@ export const ApiKeysSchema = z.object({ }); export type TApiKeys = z.infer; -export type TApiKeysInsert = Omit; -export type TApiKeysUpdate = Partial>; +export type TApiKeysInsert = Omit, TImmutableDBKeys>; +export type TApiKeysUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/audit-logs.ts b/backend/src/db/schemas/audit-logs.ts index f7143bb575..b8906698b6 100644 --- a/backend/src/db/schemas/audit-logs.ts +++ b/backend/src/db/schemas/audit-logs.ts @@ -24,5 +24,5 @@ export const AuditLogsSchema = z.object({ }); export type TAuditLogs = z.infer; -export type TAuditLogsInsert = Omit; -export type TAuditLogsUpdate = Partial>; +export type TAuditLogsInsert = Omit, TImmutableDBKeys>; +export type TAuditLogsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/auth-token-sessions.ts b/backend/src/db/schemas/auth-token-sessions.ts index 46ed7c2019..3a9376c835 100644 --- a/backend/src/db/schemas/auth-token-sessions.ts +++ b/backend/src/db/schemas/auth-token-sessions.ts @@ -20,5 +20,5 @@ export const AuthTokenSessionsSchema = z.object({ }); export type TAuthTokenSessions = z.infer; -export type TAuthTokenSessionsInsert = Omit; -export type TAuthTokenSessionsUpdate = Partial>; +export type TAuthTokenSessionsInsert = Omit, TImmutableDBKeys>; +export type TAuthTokenSessionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/auth-tokens.ts b/backend/src/db/schemas/auth-tokens.ts index 9ae8eed448..dd8563b856 100644 --- a/backend/src/db/schemas/auth-tokens.ts +++ b/backend/src/db/schemas/auth-tokens.ts @@ -21,5 +21,5 @@ export const AuthTokensSchema = z.object({ }); export type TAuthTokens = z.infer; -export type TAuthTokensInsert = Omit; -export type TAuthTokensUpdate = Partial>; +export type TAuthTokensInsert = Omit, TImmutableDBKeys>; +export type TAuthTokensUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/backup-private-key.ts b/backend/src/db/schemas/backup-private-key.ts index 5930bbd4a8..5a2148aa1d 100644 --- a/backend/src/db/schemas/backup-private-key.ts +++ b/backend/src/db/schemas/backup-private-key.ts @@ -22,5 +22,5 @@ export const BackupPrivateKeySchema = z.object({ }); export type TBackupPrivateKey = z.infer; -export type TBackupPrivateKeyInsert = Omit; -export type TBackupPrivateKeyUpdate = Partial>; +export type TBackupPrivateKeyInsert = Omit, TImmutableDBKeys>; +export type TBackupPrivateKeyUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/git-app-install-sessions.ts b/backend/src/db/schemas/git-app-install-sessions.ts index 6c6db40ea4..986ae9d8e8 100644 --- a/backend/src/db/schemas/git-app-install-sessions.ts +++ b/backend/src/db/schemas/git-app-install-sessions.ts @@ -17,5 +17,5 @@ export const GitAppInstallSessionsSchema = z.object({ }); export type TGitAppInstallSessions = z.infer; -export type TGitAppInstallSessionsInsert = Omit; -export type TGitAppInstallSessionsUpdate = Partial>; +export type TGitAppInstallSessionsInsert = Omit, TImmutableDBKeys>; +export type TGitAppInstallSessionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/git-app-org.ts b/backend/src/db/schemas/git-app-org.ts index 57e0d474a8..627df6b4cb 100644 --- a/backend/src/db/schemas/git-app-org.ts +++ b/backend/src/db/schemas/git-app-org.ts @@ -17,5 +17,5 @@ export const GitAppOrgSchema = z.object({ }); export type TGitAppOrg = z.infer; -export type TGitAppOrgInsert = Omit; -export type TGitAppOrgUpdate = Partial>; +export type TGitAppOrgInsert = Omit, TImmutableDBKeys>; +export type TGitAppOrgUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identities.ts b/backend/src/db/schemas/identities.ts index 005adf0253..adf3a6ef27 100644 --- a/backend/src/db/schemas/identities.ts +++ b/backend/src/db/schemas/identities.ts @@ -16,5 +16,5 @@ export const IdentitiesSchema = z.object({ }); export type TIdentities = z.infer; -export type TIdentitiesInsert = Omit; -export type TIdentitiesUpdate = Partial>; +export type TIdentitiesInsert = Omit, TImmutableDBKeys>; +export type TIdentitiesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-access-tokens.ts b/backend/src/db/schemas/identity-access-tokens.ts index cbd71e5c5c..18dbb81930 100644 --- a/backend/src/db/schemas/identity-access-tokens.ts +++ b/backend/src/db/schemas/identity-access-tokens.ts @@ -23,5 +23,5 @@ export const IdentityAccessTokensSchema = z.object({ }); export type TIdentityAccessTokens = z.infer; -export type TIdentityAccessTokensInsert = Omit; -export type TIdentityAccessTokensUpdate = Partial>; +export type TIdentityAccessTokensInsert = Omit, TImmutableDBKeys>; +export type TIdentityAccessTokensUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-org-memberships.ts b/backend/src/db/schemas/identity-org-memberships.ts index 647ec7124c..2f29c52e43 100644 --- a/backend/src/db/schemas/identity-org-memberships.ts +++ b/backend/src/db/schemas/identity-org-memberships.ts @@ -18,5 +18,7 @@ export const IdentityOrgMembershipsSchema = z.object({ }); export type TIdentityOrgMemberships = z.infer; -export type TIdentityOrgMembershipsInsert = Omit; -export type TIdentityOrgMembershipsUpdate = Partial>; +export type TIdentityOrgMembershipsInsert = Omit, TImmutableDBKeys>; +export type TIdentityOrgMembershipsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/identity-project-membership-role.ts b/backend/src/db/schemas/identity-project-membership-role.ts new file mode 100644 index 0000000000..90a0a35388 --- /dev/null +++ b/backend/src/db/schemas/identity-project-membership-role.ts @@ -0,0 +1,31 @@ +// 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 IdentityProjectMembershipRoleSchema = z.object({ + id: z.string().uuid(), + role: z.string(), + projectMembershipId: z.string().uuid(), + customRoleId: z.string().uuid().nullable().optional(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityProjectMembershipRole = z.infer; +export type TIdentityProjectMembershipRoleInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TIdentityProjectMembershipRoleUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/identity-project-memberships.ts b/backend/src/db/schemas/identity-project-memberships.ts index 866324c8b7..276c9581eb 100644 --- a/backend/src/db/schemas/identity-project-memberships.ts +++ b/backend/src/db/schemas/identity-project-memberships.ts @@ -18,5 +18,10 @@ export const IdentityProjectMembershipsSchema = z.object({ }); export type TIdentityProjectMemberships = z.infer; -export type TIdentityProjectMembershipsInsert = Omit; -export type TIdentityProjectMembershipsUpdate = Partial>; +export type TIdentityProjectMembershipsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TIdentityProjectMembershipsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/identity-ua-client-secrets.ts b/backend/src/db/schemas/identity-ua-client-secrets.ts index 60f8d862f2..bd549ca5b1 100644 --- a/backend/src/db/schemas/identity-ua-client-secrets.ts +++ b/backend/src/db/schemas/identity-ua-client-secrets.ts @@ -23,5 +23,7 @@ export const IdentityUaClientSecretsSchema = z.object({ }); export type TIdentityUaClientSecrets = z.infer; -export type TIdentityUaClientSecretsInsert = Omit; -export type TIdentityUaClientSecretsUpdate = Partial>; +export type TIdentityUaClientSecretsInsert = Omit, TImmutableDBKeys>; +export type TIdentityUaClientSecretsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/identity-universal-auths.ts b/backend/src/db/schemas/identity-universal-auths.ts index 5a8f0c7ec5..eeec2f666a 100644 --- a/backend/src/db/schemas/identity-universal-auths.ts +++ b/backend/src/db/schemas/identity-universal-auths.ts @@ -21,5 +21,7 @@ export const IdentityUniversalAuthsSchema = z.object({ }); export type TIdentityUniversalAuths = z.infer; -export type TIdentityUniversalAuthsInsert = Omit; -export type TIdentityUniversalAuthsUpdate = Partial>; +export type TIdentityUniversalAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityUniversalAuthsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/incident-contacts.ts b/backend/src/db/schemas/incident-contacts.ts index 431bf05ab1..23c8503b08 100644 --- a/backend/src/db/schemas/incident-contacts.ts +++ b/backend/src/db/schemas/incident-contacts.ts @@ -16,5 +16,5 @@ export const IncidentContactsSchema = z.object({ }); export type TIncidentContacts = z.infer; -export type TIncidentContactsInsert = Omit; -export type TIncidentContactsUpdate = Partial>; +export type TIncidentContactsInsert = Omit, TImmutableDBKeys>; +export type TIncidentContactsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 62b01ebd29..001fdbf180 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -8,12 +8,14 @@ export * from "./git-app-org"; export * from "./identities"; export * from "./identity-access-tokens"; export * from "./identity-org-memberships"; +export * from "./identity-project-membership-role"; export * from "./identity-project-memberships"; export * from "./identity-ua-client-secrets"; export * from "./identity-universal-auths"; export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; +export * from "./ldap-configs"; export * from "./models"; export * from "./org-bots"; export * from "./org-memberships"; @@ -24,8 +26,10 @@ export * from "./project-environments"; export * from "./project-keys"; export * from "./project-memberships"; export * from "./project-roles"; +export * from "./project-user-membership-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"; @@ -51,6 +55,7 @@ export * from "./service-tokens"; export * from "./super-admin"; export * from "./trusted-ips"; export * from "./user-actions"; +export * from "./user-aliases"; export * from "./user-encryption-keys"; export * from "./users"; export * from "./webhooks"; diff --git a/backend/src/db/schemas/integration-auths.ts b/backend/src/db/schemas/integration-auths.ts index db602c0af8..185beae366 100644 --- a/backend/src/db/schemas/integration-auths.ts +++ b/backend/src/db/schemas/integration-auths.ts @@ -33,5 +33,5 @@ export const IntegrationAuthsSchema = z.object({ }); export type TIntegrationAuths = z.infer; -export type TIntegrationAuthsInsert = Omit; -export type TIntegrationAuthsUpdate = Partial>; +export type TIntegrationAuthsInsert = Omit, TImmutableDBKeys>; +export type TIntegrationAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/integrations.ts b/backend/src/db/schemas/integrations.ts index 62f73d1901..203498c85a 100644 --- a/backend/src/db/schemas/integrations.ts +++ b/backend/src/db/schemas/integrations.ts @@ -27,9 +27,10 @@ export const IntegrationsSchema = z.object({ envId: z.string().uuid(), secretPath: z.string().default("/"), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + lastUsed: z.date().nullable().optional() }); export type TIntegrations = z.infer; -export type TIntegrationsInsert = Omit; -export type TIntegrationsUpdate = Partial>; +export type TIntegrationsInsert = Omit, TImmutableDBKeys>; +export type TIntegrationsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts new file mode 100644 index 0000000000..e3c6c8c75a --- /dev/null +++ b/backend/src/db/schemas/ldap-configs.ts @@ -0,0 +1,31 @@ +// 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 LdapConfigsSchema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + isActive: z.boolean(), + url: z.string(), + encryptedBindDN: z.string(), + bindDNIV: z.string(), + bindDNTag: z.string(), + encryptedBindPass: z.string(), + bindPassIV: z.string(), + bindPassTag: z.string(), + searchBase: z.string(), + encryptedCACert: z.string(), + caCertIV: z.string(), + caCertTag: z.string(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TLdapConfigs = z.infer; +export type TLdapConfigsInsert = Omit, TImmutableDBKeys>; +export type TLdapConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 4ef943bbe9..f85feff9c7 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -2,6 +2,7 @@ import { z } from "zod"; export enum TableName { Users = "users", + UserAliases = "user_aliases", UserEncryptionKey = "user_encryption_keys", AuthTokens = "auth_tokens", AuthTokenSession = "auth_token_sessions", @@ -19,6 +20,7 @@ export enum TableName { Environment = "project_environments", ProjectMembership = "project_memberships", ProjectRoles = "project_roles", + ProjectUserMembershipRole = "project_user_membership_roles", ProjectKeys = "project_keys", Secret = "secrets", SecretBlindIndex = "secret_blind_indexes", @@ -40,6 +42,8 @@ export enum TableName { IdentityUaClientSecret = "identity_ua_client_secrets", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", + IdentityProjectMembershipRole = "identity_project_membership_role", + ScimToken = "scim_tokens", SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalRequest = "secret_approval_requests", @@ -49,6 +53,7 @@ export enum TableName { SecretRotation = "secret_rotations", SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", + LdapConfig = "ldap_configs", AuditLog = "audit_logs", GitAppInstallSession = "git_app_install_sessions", GitAppOrg = "git_app_org", @@ -111,6 +116,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/org-bots.ts b/backend/src/db/schemas/org-bots.ts index b328f1aafb..77be907ecd 100644 --- a/backend/src/db/schemas/org-bots.ts +++ b/backend/src/db/schemas/org-bots.ts @@ -27,5 +27,5 @@ export const OrgBotsSchema = z.object({ }); export type TOrgBots = z.infer; -export type TOrgBotsInsert = Omit; -export type TOrgBotsUpdate = Partial>; +export type TOrgBotsInsert = Omit, TImmutableDBKeys>; +export type TOrgBotsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/org-memberships.ts b/backend/src/db/schemas/org-memberships.ts index b2fffa1173..585addb7ce 100644 --- a/backend/src/db/schemas/org-memberships.ts +++ b/backend/src/db/schemas/org-memberships.ts @@ -20,5 +20,5 @@ export const OrgMembershipsSchema = z.object({ }); export type TOrgMemberships = z.infer; -export type TOrgMembershipsInsert = Omit; -export type TOrgMembershipsUpdate = Partial>; +export type TOrgMembershipsInsert = Omit, TImmutableDBKeys>; +export type TOrgMembershipsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/org-roles.ts b/backend/src/db/schemas/org-roles.ts index 72b582f969..ca01c65741 100644 --- a/backend/src/db/schemas/org-roles.ts +++ b/backend/src/db/schemas/org-roles.ts @@ -19,5 +19,5 @@ export const OrgRolesSchema = z.object({ }); export type TOrgRoles = z.infer; -export type TOrgRolesInsert = Omit; -export type TOrgRolesUpdate = Partial>; +export type TOrgRolesInsert = Omit, TImmutableDBKeys>; +export type TOrgRolesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 087a1b7e01..f2933af866 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -14,9 +14,10 @@ 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; -export type TOrganizationsInsert = Omit; -export type TOrganizationsUpdate = Partial>; +export type TOrganizationsInsert = Omit, TImmutableDBKeys>; +export type TOrganizationsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/project-bots.ts b/backend/src/db/schemas/project-bots.ts index c685769438..1fa59eb781 100644 --- a/backend/src/db/schemas/project-bots.ts +++ b/backend/src/db/schemas/project-bots.ts @@ -26,5 +26,5 @@ export const ProjectBotsSchema = z.object({ }); export type TProjectBots = z.infer; -export type TProjectBotsInsert = Omit; -export type TProjectBotsUpdate = Partial>; +export type TProjectBotsInsert = Omit, TImmutableDBKeys>; +export type TProjectBotsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/project-environments.ts b/backend/src/db/schemas/project-environments.ts index 8b95dbba06..76556b7e94 100644 --- a/backend/src/db/schemas/project-environments.ts +++ b/backend/src/db/schemas/project-environments.ts @@ -18,5 +18,5 @@ export const ProjectEnvironmentsSchema = z.object({ }); export type TProjectEnvironments = z.infer; -export type TProjectEnvironmentsInsert = Omit; -export type TProjectEnvironmentsUpdate = Partial>; +export type TProjectEnvironmentsInsert = Omit, TImmutableDBKeys>; +export type TProjectEnvironmentsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/project-keys.ts b/backend/src/db/schemas/project-keys.ts index 720cd79bf0..924918b120 100644 --- a/backend/src/db/schemas/project-keys.ts +++ b/backend/src/db/schemas/project-keys.ts @@ -19,5 +19,5 @@ export const ProjectKeysSchema = z.object({ }); export type TProjectKeys = z.infer; -export type TProjectKeysInsert = Omit; -export type TProjectKeysUpdate = Partial>; +export type TProjectKeysInsert = Omit, TImmutableDBKeys>; +export type TProjectKeysUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/project-memberships.ts b/backend/src/db/schemas/project-memberships.ts index b9f191a84b..8576a318e5 100644 --- a/backend/src/db/schemas/project-memberships.ts +++ b/backend/src/db/schemas/project-memberships.ts @@ -18,5 +18,5 @@ export const ProjectMembershipsSchema = z.object({ }); export type TProjectMemberships = z.infer; -export type TProjectMembershipsInsert = Omit; -export type TProjectMembershipsUpdate = Partial>; +export type TProjectMembershipsInsert = Omit, TImmutableDBKeys>; +export type TProjectMembershipsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/project-roles.ts b/backend/src/db/schemas/project-roles.ts index 1946ab5e1b..e10f6fd4c5 100644 --- a/backend/src/db/schemas/project-roles.ts +++ b/backend/src/db/schemas/project-roles.ts @@ -19,5 +19,5 @@ export const ProjectRolesSchema = z.object({ }); export type TProjectRoles = z.infer; -export type TProjectRolesInsert = Omit; -export type TProjectRolesUpdate = Partial>; +export type TProjectRolesInsert = Omit, TImmutableDBKeys>; +export type TProjectRolesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/project-user-membership-roles.ts b/backend/src/db/schemas/project-user-membership-roles.ts new file mode 100644 index 0000000000..bc7b672084 --- /dev/null +++ b/backend/src/db/schemas/project-user-membership-roles.ts @@ -0,0 +1,31 @@ +// 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 ProjectUserMembershipRolesSchema = z.object({ + id: z.string().uuid(), + role: z.string(), + projectMembershipId: z.string().uuid(), + customRoleId: z.string().uuid().nullable().optional(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TProjectUserMembershipRoles = z.infer; +export type TProjectUserMembershipRolesInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TProjectUserMembershipRolesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 296fa421ed..3965e24c0a 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -14,9 +14,11 @@ 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; -export type TProjectsInsert = Omit; -export type TProjectsUpdate = Partial>; +export type TProjectsInsert = Omit, TImmutableDBKeys>; +export type TProjectsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/saml-configs.ts b/backend/src/db/schemas/saml-configs.ts index 6891d8adde..67171469a8 100644 --- a/backend/src/db/schemas/saml-configs.ts +++ b/backend/src/db/schemas/saml-configs.ts @@ -27,5 +27,5 @@ export const SamlConfigsSchema = z.object({ }); export type TSamlConfigs = z.infer; -export type TSamlConfigsInsert = Omit; -export type TSamlConfigsUpdate = Partial>; +export type TSamlConfigsInsert = Omit, TImmutableDBKeys>; +export type TSamlConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/scim-tokens.ts b/backend/src/db/schemas/scim-tokens.ts new file mode 100644 index 0000000000..ab6e10d27b --- /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, TImmutableDBKeys>; +export type TScimTokensUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-approval-policies-approvers.ts b/backend/src/db/schemas/secret-approval-policies-approvers.ts index 503299d30a..12a3119e6d 100644 --- a/backend/src/db/schemas/secret-approval-policies-approvers.ts +++ b/backend/src/db/schemas/secret-approval-policies-approvers.ts @@ -16,5 +16,10 @@ export const SecretApprovalPoliciesApproversSchema = z.object({ }); export type TSecretApprovalPoliciesApprovers = z.infer; -export type TSecretApprovalPoliciesApproversInsert = Omit; -export type TSecretApprovalPoliciesApproversUpdate = Partial>; +export type TSecretApprovalPoliciesApproversInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSecretApprovalPoliciesApproversUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index 6c331f1b1f..d907ef1e0f 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -18,5 +18,7 @@ export const SecretApprovalPoliciesSchema = z.object({ }); export type TSecretApprovalPolicies = z.infer; -export type TSecretApprovalPoliciesInsert = Omit; -export type TSecretApprovalPoliciesUpdate = Partial>; +export type TSecretApprovalPoliciesInsert = Omit, TImmutableDBKeys>; +export type TSecretApprovalPoliciesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-approval-request-secret-tags.ts b/backend/src/db/schemas/secret-approval-request-secret-tags.ts index f5e7ba632f..2851321e2b 100644 --- a/backend/src/db/schemas/secret-approval-request-secret-tags.ts +++ b/backend/src/db/schemas/secret-approval-request-secret-tags.ts @@ -16,5 +16,10 @@ export const SecretApprovalRequestSecretTagsSchema = z.object({ }); export type TSecretApprovalRequestSecretTags = z.infer; -export type TSecretApprovalRequestSecretTagsInsert = Omit; -export type TSecretApprovalRequestSecretTagsUpdate = Partial>; +export type TSecretApprovalRequestSecretTagsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSecretApprovalRequestSecretTagsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-approval-requests-reviewers.ts b/backend/src/db/schemas/secret-approval-requests-reviewers.ts index a3657f1f97..f3ff880473 100644 --- a/backend/src/db/schemas/secret-approval-requests-reviewers.ts +++ b/backend/src/db/schemas/secret-approval-requests-reviewers.ts @@ -17,5 +17,10 @@ export const SecretApprovalRequestsReviewersSchema = z.object({ }); export type TSecretApprovalRequestsReviewers = z.infer; -export type TSecretApprovalRequestsReviewersInsert = Omit; -export type TSecretApprovalRequestsReviewersUpdate = Partial>; +export type TSecretApprovalRequestsReviewersInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSecretApprovalRequestsReviewersUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-approval-requests-secrets.ts b/backend/src/db/schemas/secret-approval-requests-secrets.ts index 810a4f2cfe..b795b47b43 100644 --- a/backend/src/db/schemas/secret-approval-requests-secrets.ts +++ b/backend/src/db/schemas/secret-approval-requests-secrets.ts @@ -35,5 +35,10 @@ export const SecretApprovalRequestsSecretsSchema = z.object({ }); export type TSecretApprovalRequestsSecrets = z.infer; -export type TSecretApprovalRequestsSecretsInsert = Omit; -export type TSecretApprovalRequestsSecretsUpdate = Partial>; +export type TSecretApprovalRequestsSecretsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSecretApprovalRequestsSecretsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-approval-requests.ts b/backend/src/db/schemas/secret-approval-requests.ts index 590c283f59..6ee97fbb65 100644 --- a/backend/src/db/schemas/secret-approval-requests.ts +++ b/backend/src/db/schemas/secret-approval-requests.ts @@ -22,5 +22,7 @@ export const SecretApprovalRequestsSchema = z.object({ }); export type TSecretApprovalRequests = z.infer; -export type TSecretApprovalRequestsInsert = Omit; -export type TSecretApprovalRequestsUpdate = Partial>; +export type TSecretApprovalRequestsInsert = Omit, TImmutableDBKeys>; +export type TSecretApprovalRequestsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-blind-indexes.ts b/backend/src/db/schemas/secret-blind-indexes.ts index fa919babd2..474e0aa540 100644 --- a/backend/src/db/schemas/secret-blind-indexes.ts +++ b/backend/src/db/schemas/secret-blind-indexes.ts @@ -20,5 +20,5 @@ export const SecretBlindIndexesSchema = z.object({ }); export type TSecretBlindIndexes = z.infer; -export type TSecretBlindIndexesInsert = Omit; -export type TSecretBlindIndexesUpdate = Partial>; +export type TSecretBlindIndexesInsert = Omit, TImmutableDBKeys>; +export type TSecretBlindIndexesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-folder-versions.ts b/backend/src/db/schemas/secret-folder-versions.ts index 8c550d0651..8bef6e83f5 100644 --- a/backend/src/db/schemas/secret-folder-versions.ts +++ b/backend/src/db/schemas/secret-folder-versions.ts @@ -18,5 +18,5 @@ export const SecretFolderVersionsSchema = z.object({ }); export type TSecretFolderVersions = z.infer; -export type TSecretFolderVersionsInsert = Omit; -export type TSecretFolderVersionsUpdate = Partial>; +export type TSecretFolderVersionsInsert = Omit, TImmutableDBKeys>; +export type TSecretFolderVersionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-folders.ts b/backend/src/db/schemas/secret-folders.ts index 648238b0a7..0f9684d0e2 100644 --- a/backend/src/db/schemas/secret-folders.ts +++ b/backend/src/db/schemas/secret-folders.ts @@ -18,5 +18,5 @@ export const SecretFoldersSchema = z.object({ }); export type TSecretFolders = z.infer; -export type TSecretFoldersInsert = Omit; -export type TSecretFoldersUpdate = Partial>; +export type TSecretFoldersInsert = Omit, TImmutableDBKeys>; +export type TSecretFoldersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-imports.ts b/backend/src/db/schemas/secret-imports.ts index 9c1ee905f7..9d42d8da59 100644 --- a/backend/src/db/schemas/secret-imports.ts +++ b/backend/src/db/schemas/secret-imports.ts @@ -19,5 +19,5 @@ export const SecretImportsSchema = z.object({ }); export type TSecretImports = z.infer; -export type TSecretImportsInsert = Omit; -export type TSecretImportsUpdate = Partial>; +export type TSecretImportsInsert = Omit, TImmutableDBKeys>; +export type TSecretImportsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-rotation-outputs.ts b/backend/src/db/schemas/secret-rotation-outputs.ts index 3b594365a5..3ac5c2c9e9 100644 --- a/backend/src/db/schemas/secret-rotation-outputs.ts +++ b/backend/src/db/schemas/secret-rotation-outputs.ts @@ -15,5 +15,5 @@ export const SecretRotationOutputsSchema = z.object({ }); export type TSecretRotationOutputs = z.infer; -export type TSecretRotationOutputsInsert = Omit; -export type TSecretRotationOutputsUpdate = Partial>; +export type TSecretRotationOutputsInsert = Omit, TImmutableDBKeys>; +export type TSecretRotationOutputsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-rotations.ts b/backend/src/db/schemas/secret-rotations.ts index 4c65712faa..b491edc469 100644 --- a/backend/src/db/schemas/secret-rotations.ts +++ b/backend/src/db/schemas/secret-rotations.ts @@ -26,5 +26,5 @@ export const SecretRotationsSchema = z.object({ }); export type TSecretRotations = z.infer; -export type TSecretRotationsInsert = Omit; -export type TSecretRotationsUpdate = Partial>; +export type TSecretRotationsInsert = Omit, TImmutableDBKeys>; +export type TSecretRotationsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-scanning-git-risks.ts b/backend/src/db/schemas/secret-scanning-git-risks.ts index 85cfcd3768..08ba690e45 100644 --- a/backend/src/db/schemas/secret-scanning-git-risks.ts +++ b/backend/src/db/schemas/secret-scanning-git-risks.ts @@ -42,5 +42,7 @@ export const SecretScanningGitRisksSchema = z.object({ }); export type TSecretScanningGitRisks = z.infer; -export type TSecretScanningGitRisksInsert = Omit; -export type TSecretScanningGitRisksUpdate = Partial>; +export type TSecretScanningGitRisksInsert = Omit, TImmutableDBKeys>; +export type TSecretScanningGitRisksUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-snapshot-folders.ts b/backend/src/db/schemas/secret-snapshot-folders.ts index acf11ab0af..3e2853cbef 100644 --- a/backend/src/db/schemas/secret-snapshot-folders.ts +++ b/backend/src/db/schemas/secret-snapshot-folders.ts @@ -17,5 +17,5 @@ export const SecretSnapshotFoldersSchema = z.object({ }); export type TSecretSnapshotFolders = z.infer; -export type TSecretSnapshotFoldersInsert = Omit; -export type TSecretSnapshotFoldersUpdate = Partial>; +export type TSecretSnapshotFoldersInsert = Omit, TImmutableDBKeys>; +export type TSecretSnapshotFoldersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-snapshot-secrets.ts b/backend/src/db/schemas/secret-snapshot-secrets.ts index 6a83d11558..121c5d56ea 100644 --- a/backend/src/db/schemas/secret-snapshot-secrets.ts +++ b/backend/src/db/schemas/secret-snapshot-secrets.ts @@ -17,5 +17,5 @@ export const SecretSnapshotSecretsSchema = z.object({ }); export type TSecretSnapshotSecrets = z.infer; -export type TSecretSnapshotSecretsInsert = Omit; -export type TSecretSnapshotSecretsUpdate = Partial>; +export type TSecretSnapshotSecretsInsert = Omit, TImmutableDBKeys>; +export type TSecretSnapshotSecretsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-snapshots.ts b/backend/src/db/schemas/secret-snapshots.ts index ed255cb776..7f070075fc 100644 --- a/backend/src/db/schemas/secret-snapshots.ts +++ b/backend/src/db/schemas/secret-snapshots.ts @@ -17,5 +17,5 @@ export const SecretSnapshotsSchema = z.object({ }); export type TSecretSnapshots = z.infer; -export type TSecretSnapshotsInsert = Omit; -export type TSecretSnapshotsUpdate = Partial>; +export type TSecretSnapshotsInsert = Omit, TImmutableDBKeys>; +export type TSecretSnapshotsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-tag-junction.ts b/backend/src/db/schemas/secret-tag-junction.ts index 1d25574c5f..d14384fabc 100644 --- a/backend/src/db/schemas/secret-tag-junction.ts +++ b/backend/src/db/schemas/secret-tag-junction.ts @@ -14,5 +14,5 @@ export const SecretTagJunctionSchema = z.object({ }); export type TSecretTagJunction = z.infer; -export type TSecretTagJunctionInsert = Omit; -export type TSecretTagJunctionUpdate = Partial>; +export type TSecretTagJunctionInsert = Omit, TImmutableDBKeys>; +export type TSecretTagJunctionUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-tags.ts b/backend/src/db/schemas/secret-tags.ts index 622c29bd39..f94e1e2629 100644 --- a/backend/src/db/schemas/secret-tags.ts +++ b/backend/src/db/schemas/secret-tags.ts @@ -19,5 +19,5 @@ export const SecretTagsSchema = z.object({ }); export type TSecretTags = z.infer; -export type TSecretTagsInsert = Omit; -export type TSecretTagsUpdate = Partial>; +export type TSecretTagsInsert = Omit, TImmutableDBKeys>; +export type TSecretTagsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-version-tag-junction.ts b/backend/src/db/schemas/secret-version-tag-junction.ts index 2c9a24fee7..b36e28c724 100644 --- a/backend/src/db/schemas/secret-version-tag-junction.ts +++ b/backend/src/db/schemas/secret-version-tag-junction.ts @@ -14,5 +14,7 @@ export const SecretVersionTagJunctionSchema = z.object({ }); export type TSecretVersionTagJunction = z.infer; -export type TSecretVersionTagJunctionInsert = Omit; -export type TSecretVersionTagJunctionUpdate = Partial>; +export type TSecretVersionTagJunctionInsert = Omit, TImmutableDBKeys>; +export type TSecretVersionTagJunctionUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/secret-versions.ts b/backend/src/db/schemas/secret-versions.ts index d1675e3d26..d60db9b754 100644 --- a/backend/src/db/schemas/secret-versions.ts +++ b/backend/src/db/schemas/secret-versions.ts @@ -36,5 +36,5 @@ export const SecretVersionsSchema = z.object({ }); export type TSecretVersions = z.infer; -export type TSecretVersionsInsert = Omit; -export type TSecretVersionsUpdate = Partial>; +export type TSecretVersionsInsert = Omit, TImmutableDBKeys>; +export type TSecretVersionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secrets.ts b/backend/src/db/schemas/secrets.ts index 3fe5ad8d8d..f261c40bbe 100644 --- a/backend/src/db/schemas/secrets.ts +++ b/backend/src/db/schemas/secrets.ts @@ -34,5 +34,5 @@ export const SecretsSchema = z.object({ }); export type TSecrets = z.infer; -export type TSecretsInsert = Omit; -export type TSecretsUpdate = Partial>; +export type TSecretsInsert = Omit, TImmutableDBKeys>; +export type TSecretsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/service-tokens.ts b/backend/src/db/schemas/service-tokens.ts index 24720f3e4f..720c8fd6fa 100644 --- a/backend/src/db/schemas/service-tokens.ts +++ b/backend/src/db/schemas/service-tokens.ts @@ -25,5 +25,5 @@ export const ServiceTokensSchema = z.object({ }); export type TServiceTokens = z.infer; -export type TServiceTokensInsert = Omit; -export type TServiceTokensUpdate = Partial>; +export type TServiceTokensInsert = Omit, TImmutableDBKeys>; +export type TServiceTokensUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 13bf45e7bd..958fed0abf 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -12,9 +12,11 @@ export const SuperAdminSchema = z.object({ initialized: z.boolean().default(false).nullable().optional(), allowSignUp: z.boolean().default(true).nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + allowedSignUpDomain: z.string().nullable().optional(), + instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000") }); export type TSuperAdmin = z.infer; -export type TSuperAdminInsert = Omit; -export type TSuperAdminUpdate = Partial>; +export type TSuperAdminInsert = Omit, TImmutableDBKeys>; +export type TSuperAdminUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/trusted-ips.ts b/backend/src/db/schemas/trusted-ips.ts index 6d9018db3d..f9973a843b 100644 --- a/backend/src/db/schemas/trusted-ips.ts +++ b/backend/src/db/schemas/trusted-ips.ts @@ -20,5 +20,5 @@ export const TrustedIpsSchema = z.object({ }); export type TTrustedIps = z.infer; -export type TTrustedIpsInsert = Omit; -export type TTrustedIpsUpdate = Partial>; +export type TTrustedIpsInsert = Omit, TImmutableDBKeys>; +export type TTrustedIpsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/user-actions.ts b/backend/src/db/schemas/user-actions.ts index eaa03ba98d..89d2698470 100644 --- a/backend/src/db/schemas/user-actions.ts +++ b/backend/src/db/schemas/user-actions.ts @@ -16,5 +16,5 @@ export const UserActionsSchema = z.object({ }); export type TUserActions = z.infer; -export type TUserActionsInsert = Omit; -export type TUserActionsUpdate = Partial>; +export type TUserActionsInsert = Omit, TImmutableDBKeys>; +export type TUserActionsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/user-aliases.ts b/backend/src/db/schemas/user-aliases.ts new file mode 100644 index 0000000000..d8712fe751 --- /dev/null +++ b/backend/src/db/schemas/user-aliases.ts @@ -0,0 +1,24 @@ +// 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 UserAliasesSchema = z.object({ + id: z.string().uuid(), + userId: z.string().uuid(), + username: z.string(), + aliasType: z.string(), + externalId: z.string(), + emails: z.string().array().nullable().optional(), + orgId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TUserAliases = z.infer; +export type TUserAliasesInsert = Omit, TImmutableDBKeys>; +export type TUserAliasesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts index 8f35b09fe9..693b73b4c4 100644 --- a/backend/src/db/schemas/user-encryption-keys.ts +++ b/backend/src/db/schemas/user-encryption-keys.ts @@ -25,5 +25,5 @@ export const UserEncryptionKeysSchema = z.object({ }); export type TUserEncryptionKeys = z.infer; -export type TUserEncryptionKeysInsert = Omit; -export type TUserEncryptionKeysUpdate = Partial>; +export type TUserEncryptionKeysInsert = Omit, TImmutableDBKeys>; +export type TUserEncryptionKeysUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index 4a29de5100..86ee2fb74e 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -9,7 +9,7 @@ import { TImmutableDBKeys } from "./models"; export const UsersSchema = z.object({ id: z.string().uuid(), - email: z.string(), + email: z.string().nullable().optional(), authMethods: z.string().array().nullable().optional(), superAdmin: z.boolean().default(false).nullable().optional(), firstName: z.string().nullable().optional(), @@ -19,9 +19,11 @@ 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), + username: z.string() }); export type TUsers = z.infer; -export type TUsersInsert = Omit; -export type TUsersUpdate = Partial>; +export type TUsersInsert = Omit, TImmutableDBKeys>; +export type TUsersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 7abfb3772f..44aa8c5da9 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -25,5 +25,5 @@ export const WebhooksSchema = z.object({ }); export type TWebhooks = z.infer; -export type TWebhooksInsert = Omit; -export type TWebhooksUpdate = Partial>; +export type TWebhooksInsert = Omit, TImmutableDBKeys>; +export type TWebhooksUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/seed-data.ts b/backend/src/db/seed-data.ts index bb57d5bb4b..5f4ea1b4fa 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,22 @@ 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", + username: process.env.TEST_USER_USERNAME || "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 +37,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 +93,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 +101,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 +127,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 ca0042a98f..86cd2be343 100644 --- a/backend/src/db/seeds/1-user.ts +++ b/backend/src/db/seeds/1-user.ts @@ -9,13 +9,20 @@ export async function seed(knex: Knex): Promise { await knex(TableName.Users).del(); await knex(TableName.UserEncryptionKey).del(); await knex(TableName.SuperAdmin).del(); - await knex(TableName.SuperAdmin).insert([{ initialized: true, allowSignUp: true }]); + + await knex(TableName.SuperAdmin).insert([ + // eslint-disable-next-line + // @ts-ignore + { id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true } + ]); // Inserts seed entries 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, + username: seedData1.username, email: seedData1.email, superAdmin: true, firstName: "test", @@ -48,7 +55,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 a9c3ec3c77..ba2f65a36f 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 7818d58311..d41efb71c0 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 { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; +import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ { name: "Development", slug: "dev" }, @@ -20,21 +24,38 @@ 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 projectMembership = await knex(TableName.ProjectMembership) + .insert({ + projectId: project.id, + userId: seedData1.id, + role: ProjectMembershipRole.Admin + }) + .returning("*"); + await knex(TableName.ProjectUserMembershipRole).insert({ + role: ProjectMembershipRole.Admin, + projectMembershipId: projectMembership[0].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 +67,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 0000000000..618c47114e --- /dev/null +++ b/backend/src/db/seeds/4-machine-identity.ts @@ -0,0 +1,90 @@ +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 + } + ]); + + const identityProjectMembership = await knex(TableName.IdentityProjectMembership) + .insert({ + identityId: seedData1.machineIdentity.id, + projectId: seedData1.project.id, + role: ProjectMembershipRole.Admin + }) + .returning("*"); + + await knex(TableName.IdentityProjectMembershipRole).insert({ + role: ProjectMembershipRole.Admin, + projectMembershipId: identityProjectMembership[0].id + }); +} diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 2ed4393163..7d1492f84c 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,8 +1,10 @@ +import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; 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 +35,8 @@ 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(registerLdapRouter, { prefix: "/ldap" }); 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/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts new file mode 100644 index 0000000000..de472ff297 --- /dev/null +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -0,0 +1,194 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +// All the any rules are disabled because passport typesense with fastify is really poor + +import { IncomingMessage } from "node:http"; + +import { Authenticator } from "@fastify/passport"; +import fastifySession from "@fastify/session"; +import { FastifyRequest } from "fastify"; +import LdapStrategy from "passport-ldapauth"; +import { z } from "zod"; + +import { LdapConfigsSchema } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerLdapRouter = async (server: FastifyZodProvider) => { + const appCfg = getConfig(); + const passport = new Authenticator({ key: "ldap", userProperty: "passportUser" }); + await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY }); + await server.register(passport.initialize()); + await server.register(passport.secureSession()); + + const getLdapPassportOpts = (req: FastifyRequest, done: any) => { + const { organizationSlug } = req.body as { + organizationSlug: string; + }; + + process.nextTick(async () => { + try { + const { opts, ldapConfig } = await server.services.ldap.bootLdap(organizationSlug); + req.ldapConfig = ldapConfig; + done(null, opts); + } catch (err) { + done(err); + } + }); + }; + + passport.use( + new LdapStrategy( + getLdapPassportOpts as any, + // eslint-disable-next-line + async (req: IncomingMessage, user, cb) => { + try { + const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({ + externalId: user.uidNumber, + username: user.uid, + firstName: user.givenName, + lastName: user.sn, + emails: user.mail ? [user.mail] : [], + relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, + orgId: (req as unknown as FastifyRequest).ldapConfig.organization + }); + + return cb(null, { isUserCompleted, providerAuthToken }); + } catch (err) { + logger.error(err); + return cb(err, false); + } + } + ) + ); + + server.route({ + url: "/login", + method: "POST", + schema: { + body: z.object({ + organizationSlug: z.string().trim() + }) + }, + preValidation: passport.authenticate("ldapauth", { + session: false + // failureFlash: true, + // failureRedirect: "/login/provider/error" + // this is due to zod type difference + }) as any, + handler: (req, res) => { + let nextUrl; + if (req.passportUser.isUserCompleted) { + nextUrl = `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`; + } else { + nextUrl = `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`; + } + + return res.status(200).send({ + nextUrl + }); + } + }); + + server.route({ + url: "/config", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + querystring: z.object({ + organizationId: z.string().trim() + }), + response: { + 200: z.object({ + id: z.string(), + organization: z.string(), + isActive: z.boolean(), + url: z.string(), + bindDN: z.string(), + bindPass: z.string(), + searchBase: z.string(), + caCert: z.string() + }) + } + }, + handler: async (req) => { + const ldap = await server.services.ldap.getLdapCfgWithPermissionCheck({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.query.organizationId, + actorOrgId: req.permission.orgId + }); + return ldap; + } + }); + + server.route({ + url: "/config", + method: "POST", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z.object({ + organizationId: z.string().trim(), + isActive: z.boolean(), + url: z.string().trim(), + bindDN: z.string().trim(), + bindPass: z.string().trim(), + searchBase: z.string().trim(), + caCert: z.string().trim().default("") + }), + response: { + 200: LdapConfigsSchema + } + }, + handler: async (req) => { + const ldap = await server.services.ldap.createLdapCfg({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.body.organizationId, + actorOrgId: req.permission.orgId, + ...req.body + }); + + return ldap; + } + }); + + server.route({ + url: "/config", + method: "PATCH", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z + .object({ + isActive: z.boolean(), + url: z.string().trim(), + bindDN: z.string().trim(), + bindPass: z.string().trim(), + searchBase: z.string().trim(), + caCert: z.string().trim() + }) + .partial() + .merge(z.object({ organizationId: z.string() })), + response: { + 200: LdapConfigsSchema + } + }, + handler: async (req) => { + const ldap = await server.services.ldap.updateLdapCfg({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.body.organizationId, + actorOrgId: req.permission.orgId, + ...req.body + }); + + return ldap; + } + }); +}; diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 46b80e7a9d..1e40d2d806 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -1,6 +1,7 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; +import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,7 +14,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { organizationId: z.string().trim() }), body: z.object({ - slug: z.string().trim(), + slug: z + .string() + .min(1) + .trim() + .refine( + (val) => Object.keys(OrgMembershipRole).includes(val), + "Please choose a different slug, the slug you have entered is reserved" + ) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid" + }), name: z.string().trim(), description: z.string().trim().optional(), permissions: z.any().array() @@ -45,7 +56,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { roleId: z.string().trim() }), body: z.object({ - slug: z.string().trim().optional(), + slug: z + .string() + .trim() + .optional() + .refine( + (val) => typeof val === "undefined" || Object.keys(OrgMembershipRole).includes(val), + "Please choose a different slug, the slug you have entered is reserved." + ) + .refine((val) => typeof val === "undefined" || slugify(val) === val, { + message: "Slug must be a valid" + }), name: z.string().trim().optional(), description: z.string().trim().optional(), permissions: z.any().array() diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index cfcecb8f0c..6064483723 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; +import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -19,13 +20,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.workspaceId) }), querystring: z.object({ - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash), - offset: z.coerce.number().default(0), - limit: z.coerce.number().default(20) + environment: z.string().trim().describe(PROJECTS.GET_SNAPSHOTS.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(PROJECTS.GET_SNAPSHOTS.path), + offset: z.coerce.number().default(0).describe(PROJECTS.GET_SNAPSHOTS.offset), + limit: z.coerce.number().default(20).describe(PROJECTS.GET_SNAPSHOTS.limit) }), response: { 200: z.object({ @@ -89,16 +90,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId) }), querystring: z.object({ - eventType: z.nativeEnum(EventType).optional(), - userAgentType: z.nativeEnum(UserAgentType).optional(), - startDate: z.string().datetime().optional(), - endDate: z.string().datetime().optional(), - offset: z.coerce.number().default(0), - limit: z.coerce.number().default(20), - actor: z.string().optional() + eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), + userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType), + startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), + endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), + offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), + limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit), + actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 00dd09c330..fe387143ee 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -27,6 +27,7 @@ type TSAMLConfig = { cert: string; audience: string; wantAuthnResponseSigned?: boolean; + wantAssertionsSigned?: boolean; disableRequestedAuthnContext?: boolean; }; @@ -82,6 +83,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { samlConfig.audience = `spn:${ssoConfig.issuer}`; } } + if (ssoConfig.authProvider === SamlProviders.GOOGLE_SAML) { + samlConfig.wantAssertionsSigned = false; + } + (req as unknown as FastifyRequest).ssoConfig = ssoConfig; done(null, samlConfig); } catch (error) { @@ -94,14 +99,14 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { async (req, profile, cb) => { try { if (!profile) throw new BadRequestError({ message: "Missing profile" }); - const { firstName } = profile; const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved - if (!email || !firstName) { + if (!profile.email || !profile.firstName) { throw new BadRequestError({ message: "Invalid request. Missing email or first name" }); } const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ + username: profile.nameID ?? email, email, firstName: profile.firstName as string, lastName: profile.lastName as string, 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 0000000000..2a3772cd6e --- /dev/null +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -0,0 +1,336 @@ +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" }, (_, 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(), + 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(), + 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( + z.object({ + primary: z.boolean(), + value: z.string().email(), + type: z.string().trim() + }) + ) + .optional(), + // 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 primaryEmail = req.body.emails?.find((email) => email.primary)?.value; + + const user = await req.server.services.scim.createScimUser({ + username: req.body.userName, + email: primaryEmail, + 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/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 0b858255f7..79161bfd30 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas"; +import { PROJECTS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -65,7 +66,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretSnapshotId: z.string().trim() + secretSnapshotId: z.string().trim().describe(PROJECTS.ROLLBACK_TO_SNAPSHOT.secretSnapshotId) }), response: { 200: z.object({ diff --git a/backend/src/ee/services/audit-log/audit-log-queue.ts b/backend/src/ee/services/audit-log/audit-log-queue.ts index 6f2c93221a..afffd463da 100644 --- a/backend/src/ee/services/audit-log/audit-log-queue.ts +++ b/backend/src/ee/services/audit-log/audit-log-queue.ts @@ -24,7 +24,7 @@ export const auditLogQueueServiceFactory = ({ const pushToLog = async (data: TCreateAuditLogDTO) => { await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { removeOnFail: { - count: 5 + count: 3 }, removeOnComplete: true }); @@ -46,6 +46,7 @@ export const auditLogQueueServiceFactory = ({ const ttl = plan.auditLogsRetentionDays * MS_IN_DAY; // skip inserting if audit log retention is 0 meaning its not supported if (ttl === 0) return; + await auditLogDAL.create({ actor: actor.type, actorMetadata: actor.metadata, 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 1c7868fc28..c4d4aabc01 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 011dffebea..220c250027 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; @@ -92,7 +92,8 @@ export enum EventType { interface UserActorMetadata { userId: string; - email: string; + email?: string | null; + username: string; } interface ServiceActorMetadata { @@ -105,6 +106,8 @@ interface IdentityActorMetadata { name: string; } +interface ScimClientActorMetadata {} + export interface UserActor { type: ActorType.USER; metadata: UserActorMetadata; @@ -120,7 +123,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/ldap-config/ldap-config-dal.ts b/backend/src/ee/services/ldap-config/ldap-config-dal.ts new file mode 100644 index 0000000000..d05747c36c --- /dev/null +++ b/backend/src/ee/services/ldap-config/ldap-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TLdapConfigDALFactory = ReturnType; + +export const ldapConfigDALFactory = (db: TDbClient) => { + const ldapCfgOrm = ormify(db, TableName.LdapConfig); + + return { ...ldapCfgOrm }; +}; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts new file mode 100644 index 0000000000..e9ae0264aa --- /dev/null +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -0,0 +1,429 @@ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; +import { + decryptSymmetric, + encryptSymmetric, + generateAsymmetricKeyPair, + generateSymmetricKey, + infisicalSymmetricDecrypt, + infisicalSymmetricEncypt +} from "@app/lib/crypto/encryption"; +import { BadRequestError } from "@app/lib/errors"; +import { TOrgPermission } from "@app/lib/types"; +import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; +import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; +import { normalizeUsername } from "@app/services/user/user-fns"; +import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TLdapConfigDALFactory } from "./ldap-config-dal"; +import { TCreateLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types"; + +type TLdapConfigServiceFactoryDep = { + ldapConfigDAL: TLdapConfigDALFactory; + orgDAL: Pick< + TOrgDALFactory, + "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" + >; + orgBotDAL: Pick; + userDAL: Pick; + userAliasDAL: Pick; + permissionService: Pick; + licenseService: Pick; +}; + +export type TLdapConfigServiceFactory = ReturnType; + +export const ldapConfigServiceFactory = ({ + ldapConfigDAL, + orgDAL, + orgBotDAL, + userDAL, + userAliasDAL, + permissionService, + licenseService +}: TLdapConfigServiceFactoryDep) => { + const createLdapCfg = async ({ + actor, + actorId, + orgId, + actorOrgId, + isActive, + url, + bindDN, + bindPass, + searchBase, + caCert + }: TCreateLdapCfgDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); + + const plan = await licenseService.getPlan(orgId); + if (!plan.ldap) + throw new BadRequestError({ + message: + "Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." + }); + + const orgBot = await orgBotDAL.transaction(async (tx) => { + const doc = await orgBotDAL.findOne({ orgId }, tx); + if (doc) return doc; + + const { privateKey, publicKey } = generateAsymmetricKeyPair(); + const key = generateSymmetricKey(); + const { + ciphertext: encryptedPrivateKey, + iv: privateKeyIV, + tag: privateKeyTag, + encoding: privateKeyKeyEncoding, + algorithm: privateKeyAlgorithm + } = infisicalSymmetricEncypt(privateKey); + const { + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + encoding: symmetricKeyKeyEncoding, + algorithm: symmetricKeyAlgorithm + } = infisicalSymmetricEncypt(key); + + return orgBotDAL.create( + { + name: "Infisical org bot", + publicKey, + privateKeyIV, + encryptedPrivateKey, + symmetricKeyIV, + symmetricKeyTag, + encryptedSymmetricKey, + symmetricKeyAlgorithm, + orgId, + privateKeyTag, + privateKeyAlgorithm, + privateKeyKeyEncoding, + symmetricKeyKeyEncoding + }, + tx + ); + }); + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); + const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); + const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); + + const ldapConfig = await ldapConfigDAL.create({ + orgId, + isActive, + url, + encryptedBindDN, + bindDNIV, + bindDNTag, + encryptedBindPass, + bindPassIV, + bindPassTag, + searchBase, + encryptedCACert, + caCertIV, + caCertTag + }); + + return ldapConfig; + }; + + const updateLdapCfg = async ({ + actor, + actorId, + orgId, + actorOrgId, + isActive, + url, + bindDN, + bindPass, + searchBase, + caCert + }: TUpdateLdapCfgDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); + + const plan = await licenseService.getPlan(orgId); + if (!plan.ldap) + throw new BadRequestError({ + message: + "Failed to update LDAP configuration due to plan restriction. Upgrade plan to update LDAP configuration." + }); + + const updateQuery: TLdapConfigsUpdate = { + isActive, + url, + searchBase + }; + + const orgBot = await orgBotDAL.findOne({ orgId }); + if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + if (bindDN !== undefined) { + const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); + updateQuery.encryptedBindDN = encryptedBindDN; + updateQuery.bindDNIV = bindDNIV; + updateQuery.bindDNTag = bindDNTag; + } + + if (bindPass !== undefined) { + const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); + updateQuery.encryptedBindPass = encryptedBindPass; + updateQuery.bindPassIV = bindPassIV; + updateQuery.bindPassTag = bindPassTag; + } + + if (caCert !== undefined) { + const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); + updateQuery.encryptedCACert = encryptedCACert; + updateQuery.caCertIV = caCertIV; + updateQuery.caCertTag = caCertTag; + } + + const [ldapConfig] = await ldapConfigDAL.update({ orgId }, updateQuery); + + return ldapConfig; + }; + + const getLdapCfg = async (filter: { orgId: string; isActive?: boolean }) => { + const ldapConfig = await ldapConfigDAL.findOne(filter); + if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); + + const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId }); + if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { + encryptedBindDN, + bindDNIV, + bindDNTag, + encryptedBindPass, + bindPassIV, + bindPassTag, + encryptedCACert, + caCertIV, + caCertTag + } = ldapConfig; + + let bindDN = ""; + if (encryptedBindDN && bindDNIV && bindDNTag) { + bindDN = decryptSymmetric({ + ciphertext: encryptedBindDN, + key, + tag: bindDNTag, + iv: bindDNIV + }); + } + + let bindPass = ""; + if (encryptedBindPass && bindPassIV && bindPassTag) { + bindPass = decryptSymmetric({ + ciphertext: encryptedBindPass, + key, + tag: bindPassTag, + iv: bindPassIV + }); + } + + let caCert = ""; + if (encryptedCACert && caCertIV && caCertTag) { + caCert = decryptSymmetric({ + ciphertext: encryptedCACert, + key, + tag: caCertTag, + iv: caCertIV + }); + } + + return { + id: ldapConfig.id, + organization: ldapConfig.orgId, + isActive: ldapConfig.isActive, + url: ldapConfig.url, + bindDN, + bindPass, + searchBase: ldapConfig.searchBase, + caCert + }; + }; + + const getLdapCfgWithPermissionCheck = async ({ actor, actorId, orgId, actorOrgId }: TOrgPermission) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); + return getLdapCfg({ + orgId + }); + }; + + const bootLdap = async (organizationSlug: string) => { + const organization = await orgDAL.findOne({ slug: organizationSlug }); + if (!organization) throw new BadRequestError({ message: "Org not found" }); + + const ldapConfig = await getLdapCfg({ + orgId: organization.id, + isActive: true + }); + + const opts = { + server: { + url: ldapConfig.url, + bindDN: ldapConfig.bindDN, + bindCredentials: ldapConfig.bindPass, + searchBase: ldapConfig.searchBase, + searchFilter: "(uid={{username}})", + searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"], + ...(ldapConfig.caCert !== "" + ? { + tlsOptions: { + ca: [ldapConfig.caCert] + } + } + : {}) + }, + passReqToCallback: true + }; + + return { opts, ldapConfig }; + }; + + const ldapLogin = async ({ externalId, username, firstName, lastName, emails, orgId, relayState }: TLdapLoginDTO) => { + const appCfg = getConfig(); + let userAlias = await userAliasDAL.findOne({ + externalId, + orgId, + aliasType: AuthMethod.LDAP + }); + + const organization = await orgDAL.findOrgById(orgId); + if (!organization) throw new BadRequestError({ message: "Org not found" }); + + if (userAlias) { + await userDAL.transaction(async (tx) => { + const [orgMembership] = await orgDAL.findMembership({ userId: userAlias.userId }, { tx }); + if (!orgMembership) { + await orgDAL.createMembership( + { + userId: userAlias.userId, + orgId, + role: OrgMembershipRole.Member, + status: OrgMembershipStatus.Accepted + }, + tx + ); + } else if (orgMembership.status === OrgMembershipStatus.Invited) { + await orgDAL.updateMembershipById( + orgMembership.id, + { + status: OrgMembershipStatus.Accepted + }, + tx + ); + } + }); + } else { + userAlias = await userDAL.transaction(async (tx) => { + const uniqueUsername = await normalizeUsername(username, userDAL); + const newUser = await userDAL.create( + { + username: uniqueUsername, + email: emails[0], + firstName, + lastName, + authMethods: [AuthMethod.LDAP], + isGhost: false + }, + tx + ); + const newUserAlias = await userAliasDAL.create( + { + userId: newUser.id, + username, + aliasType: AuthMethod.LDAP, + externalId, + emails, + orgId + }, + tx + ); + + await orgDAL.createMembership( + { + userId: newUser.id, + orgId, + role: OrgMembershipRole.Member, + status: OrgMembershipStatus.Invited + }, + tx + ); + + return newUserAlias; + }); + } + + const user = await userDAL.findOne({ id: userAlias.userId }); + + const isUserCompleted = Boolean(user.isAccepted); + + const providerAuthToken = jwt.sign( + { + authTokenType: AuthTokenType.PROVIDER_TOKEN, + userId: user.id, + username: user.username, + firstName, + lastName, + organizationName: organization.name, + organizationId: organization.id, + authMethod: AuthMethod.LDAP, + isUserCompleted, + ...(relayState + ? { + callbackPort: (JSON.parse(relayState) as { callbackPort: string }).callbackPort + } + : {}) + }, + appCfg.AUTH_SECRET, + { + expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME + } + ); + + return { isUserCompleted, providerAuthToken }; + }; + + return { + createLdapCfg, + updateLdapCfg, + getLdapCfgWithPermissionCheck, + getLdapCfg, + // getLdapPassportOpts, + ldapLogin, + bootLdap + }; +}; diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts new file mode 100644 index 0000000000..025ce7781e --- /dev/null +++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts @@ -0,0 +1,30 @@ +import { TOrgPermission } from "@app/lib/types"; + +export type TCreateLdapCfgDTO = { + isActive: boolean; + url: string; + bindDN: string; + bindPass: string; + searchBase: string; + caCert: string; +} & TOrgPermission; + +export type TUpdateLdapCfgDTO = Partial<{ + isActive: boolean; + url: string; + bindDN: string; + bindPass: string; + searchBase: string; + caCert: string; +}> & + TOrgPermission; + +export type TLdapLoginDTO = { + externalId: string; + username: string; + firstName: string; + lastName: string; + emails: string[]; + orgId: string; + relayState?: string; +}; 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 0000000000..8f52939c5b --- /dev/null +++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts @@ -0,0 +1,29 @@ +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, + scim: false, + ldap: 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 7014eac5f3..8dca967370 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -24,6 +24,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ auditLogs: false, auditLogsRetentionDays: 0, samlSSO: false, + scim: false, + ldap: false, status: null, trial_end: null, has_used_trial: true, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 6d97b537ca..a55f2edffd 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -5,9 +5,10 @@ // TODO(akhilmhdh): With tony find out the api structure and fill it here import { ForbiddenError } from "@casl/ability"; -import NodeCache from "node-cache"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; +import { verifyOfflineLicense } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TOrgDALFactory } from "@app/services/org/org-dal"; @@ -26,6 +27,7 @@ import { TFeatureSet, TGetOrgBillInfoDTO, TGetOrgTaxIdDTO, + TOfflineLicenseContents, TOrgInvoiceDTO, TOrgLicensesDTO, TOrgPlanDTO, @@ -39,6 +41,7 @@ type TLicenseServiceFactoryDep = { orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; + keyStore: Pick; }; export type TLicenseServiceFactory = ReturnType; @@ -46,12 +49,18 @@ export type TLicenseServiceFactory = ReturnType; const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login"; const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login"; -const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`; -export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => { +const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second +const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`; + +export const licenseServiceFactory = ({ + orgDAL, + permissionService, + licenseDAL, + keyStore +}: TLicenseServiceFactoryDep) => { let isValidLicense = false; let instanceType = InstanceType.OnPrem; let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); - const featureStore = new NodeCache({ stdTTL: 60 }); const appCfg = getConfig(); const licenseServerCloudApi = setupLicenceRequestWithStore( @@ -75,6 +84,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: isValidLicense = true; return; } + if (appCfg.LICENSE_KEY) { const token = await licenseServerOnPremApi.refreshLicence(); if (token) { @@ -88,6 +98,36 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: } return; } + + if (appCfg.LICENSE_KEY_OFFLINE) { + let isValidOfflineLicense = true; + const contents: TOfflineLicenseContents = JSON.parse( + Buffer.from(appCfg.LICENSE_KEY_OFFLINE, "base64").toString("utf8") + ); + const isVerified = await verifyOfflineLicense(JSON.stringify(contents.license), contents.signature); + + if (!isVerified) { + isValidOfflineLicense = false; + logger.warn(`Infisical EE offline license verification failed`); + } + + if (contents.license.terminatesAt) { + const terminationDate = new Date(contents.license.terminatesAt); + if (terminationDate < new Date()) { + isValidOfflineLicense = false; + logger.warn(`Infisical EE offline license has expired`); + } + } + + if (isValidOfflineLicense) { + onPremFeatures = contents.license.features; + instanceType = InstanceType.EnterpriseOnPrem; + logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`); + isValidLicense = true; + return; + } + } + // this means this is self hosted oss version // else it would reach catch statement isValidLicense = true; @@ -100,22 +140,21 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`); try { if (instanceType === InstanceType.Cloud) { - const cachedPlan = featureStore.get(FEATURE_CACHE_KEY(orgId, projectId)); - if (cachedPlan) return cachedPlan; + const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId)); + if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet; const org = await orgDAL.findOrgById(orgId); if (!org) throw new BadRequestError({ message: "Org not found" }); const { data: { currentPlan } } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( - `/api/license-server/v1/customers/${org.customerId}/cloud-plan`, - { - params: { - workspaceId: projectId - } - } + `/api/license-server/v1/customers/${org.customerId}/cloud-plan` + ); + await keyStore.setItemWithExpiry( + FEATURE_CACHE_KEY(org.id), + LICENSE_SERVER_CLOUD_PLAN_TTL, + JSON.stringify(currentPlan) ); - featureStore.set(FEATURE_CACHE_KEY(org.id, projectId), currentPlan); return currentPlan; } } catch (error) { @@ -123,26 +162,31 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: `getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`, error ); + await keyStore.setItemWithExpiry( + FEATURE_CACHE_KEY(orgId), + LICENSE_SERVER_CLOUD_PLAN_TTL, + JSON.stringify(onPremFeatures) + ); return onPremFeatures; } return onPremFeatures; }; - const refreshPlan = async (orgId: string, projectId?: string) => { + const refreshPlan = async (orgId: string) => { if (instanceType === InstanceType.Cloud) { - featureStore.del(FEATURE_CACHE_KEY(orgId, projectId)); - await getPlan(orgId, projectId); + await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); + await getPlan(orgId); } }; - const generateOrgCustomerId = async (orgName: string, email: string) => { + const generateOrgCustomerId = async (orgName: string, email?: string | null) => { if (instanceType === InstanceType.Cloud) { const { data: { customerId } } = await licenseServerCloudApi.request.post<{ customerId: string }>( "/api/license-server/v1/customers", { - email, + email: email ?? "", name: orgName }, { timeout: 5000, signal: AbortSignal.timeout(5000) } @@ -166,7 +210,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: quantity: count }); } - featureStore.del(orgId); + await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); } else if (instanceType === InstanceType.EnterpriseOnPrem) { const usedSeats = await licenseDAL.countOfOrgMembers(null); await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats }); @@ -215,7 +259,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: `/api/license-server/v1/customers/${organization.customerId}/session/trial`, { success_url } ); - featureStore.del(FEATURE_CACHE_KEY(orgId)); + await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); return { url }; }; @@ -505,6 +549,9 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: get isValidLicense() { return isValidLicense; }, + getInstanceType() { + return instanceType; + }, getPlan, updateSubscriptionOrgMemberCount, refreshPlan, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 762aff5b21..f8ed8aff34 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -6,6 +6,21 @@ export enum InstanceType { Cloud = "cloud" } +export type TOfflineLicenseContents = { + license: TOfflineLicense; + signature: string; +}; + +export type TOfflineLicense = { + issuedTo: string; + licenseId: string; + customerId: string | null; + issuedAt: string; + expiresAt: string | null; + terminatesAt: string | null; + features: TFeatureSet; +}; + export type TFeatureSet = { _id: null; slug: null; @@ -25,6 +40,8 @@ export type TFeatureSet = { auditLogs: false; auditLogsRetentionDays: 0; samlSSO: false; + scim: false; + ldap: 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 cc18af8aea..30b601c2c0 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -16,6 +16,8 @@ export enum OrgPermissionSubjects { Settings = "settings", IncidentAccount = "incident-contact", Sso = "sso", + Scim = "scim", + Ldap = "ldap", Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity" @@ -29,6 +31,8 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.Sso] + | [OrgPermissionActions, OrgPermissionSubjects.Scim] + | [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity]; @@ -69,6 +73,16 @@ 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.Ldap); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index ea195bc06e..d94589b43d 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -1,7 +1,9 @@ +import { z } from "zod"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { IdentityProjectMembershipRoleSchema, ProjectUserMembershipRolesSchema, TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { selectAllTableCols } from "@app/lib/knex"; +import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TPermissionDALFactory = ReturnType; @@ -43,21 +45,72 @@ export const permissionDALFactory = (db: TDbClient) => { const getProjectPermission = async (userId: string, projectId: string) => { try { - const membership = await db(TableName.ProjectMembership) - .leftJoin(TableName.ProjectRoles, `${TableName.ProjectMembership}.roleId`, `${TableName.ProjectRoles}.id`) + const docs = await db(TableName.ProjectMembership) + .join( + TableName.ProjectUserMembershipRole, + `${TableName.ProjectUserMembershipRole}.projectMembershipId`, + `${TableName.ProjectMembership}.id` + ) + .leftJoin( + TableName.ProjectRoles, + `${TableName.ProjectUserMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .where("userId", userId) .where(`${TableName.ProjectMembership}.projectId`, projectId) - .select(selectAllTableCols(TableName.ProjectMembership)) + .select(selectAllTableCols(TableName.ProjectUserMembershipRole)) .select( + db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + // TODO(roll-forward-migration): remove this field when we drop this in next migration after a week + db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"), + db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), - db.ref("orgId").withSchema(TableName.Project) + db.ref("orgId").withSchema(TableName.Project), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") ) - .select("permissions") - .first(); + .select("permissions"); - return membership; + const permission = sqlNestRelationships({ + data: docs, + key: "membershipId", + parentMapper: ({ + orgId, + orgAuthEnforced, + membershipId, + membershipCreatedAt, + membershipUpdatedAt, + oldRoleField + }) => ({ + orgId, + orgAuthEnforced, + userId, + role: oldRoleField, + id: membershipId, + projectId, + createdAt: membershipCreatedAt, + updatedAt: membershipUpdatedAt + }), + childrenMapper: [ + { + key: "id", + label: "roles" as const, + mapper: (data) => + ProjectUserMembershipRolesSchema.extend({ + permissions: z.unknown(), + customRoleSlug: z.string().optional().nullable() + }).parse(data) + } + ] + }); + // when introducting cron mode change it here + const activeRoles = permission?.[0]?.roles.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; } catch (error) { throw new DatabaseError({ error, name: "GetProjectPermission" }); } @@ -65,18 +118,62 @@ export const permissionDALFactory = (db: TDbClient) => { const getProjectIdentityPermission = async (identityId: string, projectId: string) => { try { - const membership = await db(TableName.IdentityProjectMembership) + const docs = await db(TableName.IdentityProjectMembership) + .join( + TableName.IdentityProjectMembershipRole, + `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) .leftJoin( TableName.ProjectRoles, - `${TableName.IdentityProjectMembership}.roleId`, + `${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) .where("identityId", identityId) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .select(selectAllTableCols(TableName.IdentityProjectMembership)) - .select("permissions") - .first(); - return membership; + .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) + .select( + db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), + db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"), + db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") + ) + .select("permissions"); + + const permission = sqlNestRelationships({ + data: docs, + key: "membershipId", + parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, oldRoleField }) => ({ + id: membershipId, + identityId, + projectId, + role: oldRoleField, + createdAt: membershipCreatedAt, + updatedAt: membershipUpdatedAt, + // just a prefilled value + orgAuthEnforced: false, + orgId: "" + }), + childrenMapper: [ + { + key: "id", + label: "roles" as const, + mapper: (data) => + IdentityProjectMembershipRoleSchema.extend({ + permissions: z.unknown(), + customRoleSlug: z.string().optional().nullable() + }).parse(data) + } + ] + }); + + // when introducting cron mode change it here + const activeRoles = permission?.[0]?.roles.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; } catch (error) { throw new DatabaseError({ error, name: "GetProjectIdentityPermission" }); } diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 4735312e40..c7dcf4b8cd 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -18,6 +18,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; +import { TBuildProjectPermissionDTO } from "./permission-types"; import { buildServiceTokenProjectPermission, projectAdminPermissions, @@ -64,31 +65,35 @@ export const permissionServiceFactory = ({ } }; - const buildProjectPermission = (role: string, permission?: unknown) => { - switch (role) { - case ProjectMembershipRole.Admin: - return projectAdminPermissions; - case ProjectMembershipRole.Member: - return projectMemberPermissions; - case ProjectMembershipRole.Viewer: - return projectViewerPermission; - case ProjectMembershipRole.NoAccess: - return projectNoAccessPermissions; - case ProjectMembershipRole.Custom: - return createMongoAbility( - unpackRules>>( - permission as PackRule>>[] - ), - { - conditionsMatcher + const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => { + const rules = projectUserRoles + .map(({ role, permissions }) => { + switch (role) { + case ProjectMembershipRole.Admin: + return projectAdminPermissions; + case ProjectMembershipRole.Member: + return projectMemberPermissions; + case ProjectMembershipRole.Viewer: + return projectViewerPermission; + case ProjectMembershipRole.NoAccess: + return projectNoAccessPermissions; + case ProjectMembershipRole.Custom: { + return unpackRules>>( + permissions as PackRule>>[] + ); } - ); - default: - throw new BadRequestError({ - name: "ProjectRoleInvalid", - message: "Project role not found" - }); - } + default: + throw new BadRequestError({ + name: "ProjectRoleInvalid", + message: "Project role not found" + }); + } + }) + .reduce((curr, prev) => prev.concat(curr), []); + + return createMongoAbility(rules, { + conditionsMatcher + }); }; /* @@ -145,38 +150,63 @@ export const permissionServiceFactory = ({ }; // user permission for a project in an organization - const getUserProjectPermission = async (userId: string, projectId: string, userOrgId?: string) => { - const membership = await permissionDAL.getProjectPermission(userId, projectId); - if (!membership) throw new UnauthorizedError({ name: "User not in project" }); - if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) { + const getUserProjectPermission = async ( + userId: string, + projectId: string, + userOrgId?: string + ): Promise> => { + const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); + if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" }); + + if ( + userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions) + ) { throw new BadRequestError({ name: "Custom permission not found" }); } - if (membership.orgAuthEnforced && membership.orgId !== userOrgId) { + if (userProjectPermission.orgAuthEnforced && userProjectPermission.orgId !== userOrgId) { throw new BadRequestError({ name: "Cannot access org-scoped resource" }); } return { - permission: buildProjectPermission(membership.role, membership.permissions), - membership + permission: buildProjectPermission(userProjectPermission.roles), + membership: userProjectPermission, + hasRole: (role: string) => + userProjectPermission.roles.findIndex( + ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug + ) !== -1 }; }; - const getIdentityProjectPermission = async (identityId: string, projectId: string) => { - const membership = await permissionDAL.getProjectIdentityPermission(identityId, projectId); - if (!membership) throw new UnauthorizedError({ name: "Identity not in project" }); - if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) { + const getIdentityProjectPermission = async ( + identityId: string, + projectId: string + ): Promise> => { + const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); + if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" }); + + if ( + identityProjectPermission.roles.some( + ({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions + ) + ) { throw new BadRequestError({ name: "Custom permission not found" }); } return { - permission: buildProjectPermission(membership.role, membership.permissions), - membership + permission: buildProjectPermission(identityProjectPermission.roles), + membership: identityProjectPermission, + hasRole: (role: string) => + identityProjectPermission.roles.findIndex( + ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug + ) !== -1 }; }; const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => { const serviceToken = await serviceTokenDAL.findById(serviceTokenId); + if (!serviceToken) throw new BadRequestError({ message: "Service token not found" }); + if (serviceToken.projectId !== projectId) throw new UnauthorizedError({ message: "Failed to find service authorization for given project" @@ -189,14 +219,19 @@ export const permissionServiceFactory = ({ }; type TProjectPermissionRT = T extends ActorType.SERVICE - ? { permission: MongoAbility; membership: undefined } + ? { + permission: MongoAbility; + membership: undefined; + hasRole: (arg: string) => boolean; + } // service token doesn't have both membership and roles : { permission: MongoAbility; membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & { - orgAuthEnforced: boolean; + orgAuthEnforced: boolean | null | undefined; orgId: string; - permissions?: unknown; + roles: Array<{ role: string }>; }; + hasRole: (role: string) => boolean; }; const getProjectPermission = async ( @@ -226,11 +261,13 @@ export const permissionServiceFactory = ({ const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); if (!projectRole) throw new BadRequestError({ message: "Role not found" }); return { - permission: buildProjectPermission(ProjectMembershipRole.Custom, projectRole.permissions), + permission: buildProjectPermission([ + { role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } + ]), role: projectRole }; } - return { permission: buildProjectPermission(role, []) }; + return { permission: buildProjectPermission([{ role, permissions: [] }]) }; }; return { diff --git a/backend/src/ee/services/permission/permission-types.ts b/backend/src/ee/services/permission/permission-types.ts index e69de29bb2..a35958ffd8 100644 --- a/backend/src/ee/services/permission/permission-types.ts +++ b/backend/src/ee/services/permission/permission-types.ts @@ -0,0 +1,4 @@ +export type TBuildProjectPermissionDTO = { + permissions?: unknown; + role: string; +}[]; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 5245c26e47..46dbdcc3b3 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -56,8 +56,8 @@ export type ProjectPermissionSet = | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; -const buildAdminPermission = () => { - const { can, build } = new AbilityBuilder>(createMongoAbility); +const buildAdminPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); @@ -135,13 +135,13 @@ const buildAdminPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); - return build({ conditionsMatcher }); + return rules; }; -export const projectAdminPermissions = buildAdminPermission(); +export const projectAdminPermissions = buildAdminPermissionRules(); -const buildMemberPermission = () => { - const { can, build } = new AbilityBuilder>(createMongoAbility); +const buildMemberPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); @@ -196,13 +196,13 @@ const buildMemberPermission = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); - return build({ conditionsMatcher }); + return rules; }; -export const projectMemberPermissions = buildMemberPermission(); +export const projectMemberPermissions = buildMemberPermissionRules(); -const buildViewerPermission = () => { - const { can, build } = new AbilityBuilder>(createMongoAbility); +const buildViewerPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); @@ -220,14 +220,14 @@ const buildViewerPermission = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); - return build({ conditionsMatcher }); + return rules; }; -export const projectViewerPermission = buildViewerPermission(); +export const projectViewerPermission = buildViewerPermissionRules(); const buildNoAccessProjectPermission = () => { - const { build } = new AbilityBuilder>(createMongoAbility); - return build({ conditionsMatcher }); + const { rules } = new AbilityBuilder>(createMongoAbility); + return rules; }; export const buildServiceTokenProjectPermission = ( 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 767729179a..e9249d4aa4 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -5,6 +5,7 @@ import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, + TableName, TSamlConfigs, TSamlConfigsUpdate } from "@app/db/schemas"; @@ -31,7 +32,7 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f type TSamlConfigServiceFactoryDep = { samlConfigDAL: TSamlConfigDALFactory; - userDAL: Pick; + userDAL: Pick; orgDAL: Pick< TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" @@ -69,7 +70,7 @@ export const samlConfigServiceFactory = ({ if (!plan.samlSSO) throw new BadRequestError({ message: - "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." + "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." }); const orgBot = await orgBotDAL.transaction(async (tx) => { @@ -122,7 +123,6 @@ export const samlConfigServiceFactory = ({ const { ciphertext: encryptedEntryPoint, iv: entryPointIV, tag: entryPointTag } = encryptSymmetric(entryPoint, key); const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); - const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); const samlConfig = await samlConfigDAL.create({ orgId, @@ -172,7 +172,7 @@ export const samlConfigServiceFactory = ({ keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding }); - if (entryPoint) { + if (entryPoint !== undefined) { const { ciphertext: encryptedEntryPoint, iv: entryPointIV, @@ -182,20 +182,21 @@ export const samlConfigServiceFactory = ({ updateQuery.entryPointIV = entryPointIV; updateQuery.entryPointTag = entryPointTag; } - if (issuer) { + if (issuer !== undefined) { const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); updateQuery.encryptedIssuer = encryptedIssuer; updateQuery.issuerIV = issuerIV; updateQuery.issuerTag = issuerTag; } - if (cert) { + if (cert !== undefined) { const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); updateQuery.encryptedCert = encryptedCert; updateQuery.certIV = certIV; 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; }; @@ -300,16 +301,30 @@ export const samlConfigServiceFactory = ({ }; }; - const samlLogin = async ({ firstName, email, lastName, authProvider, orgId, relayState }: TSamlLoginDTO) => { + const samlLogin = async ({ + username, + email, + firstName, + lastName, + authProvider, + orgId, + relayState + }: TSamlLoginDTO) => { const appCfg = getConfig(); - let user = await userDAL.findUserByEmail(email); + let user = await userDAL.findOne({ username }); const organization = await orgDAL.findOrgById(orgId); if (!organization) throw new BadRequestError({ message: "Org not found" }); if (user) { await userDAL.transaction(async (tx) => { - const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx }); + const [orgMembership] = await orgDAL.findMembership( + { + userId: user.id, + [`${TableName.OrgMembership}.orgId` as "id"]: orgId + }, + { tx } + ); if (!orgMembership) { await orgDAL.createMembership( { @@ -335,10 +350,12 @@ export const samlConfigServiceFactory = ({ user = await userDAL.transaction(async (tx) => { const newUser = await userDAL.create( { + username, email, firstName, lastName, - authMethods: [AuthMethod.EMAIL] + authMethods: [AuthMethod.EMAIL], + isGhost: false }, tx ); @@ -356,7 +373,7 @@ export const samlConfigServiceFactory = ({ { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, - email: user.email, + username: user.username, firstName, lastName, organizationName: organization.name, diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index a2c2c63c05..ec7c066fca 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -4,7 +4,8 @@ import { ActorType } from "@app/services/auth/auth-type"; export enum SamlProviders { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", - JUMPCLOUD_SAML = "jumpcloud-saml" + JUMPCLOUD_SAML = "jumpcloud-saml", + GOOGLE_SAML = "google-saml" } export type TCreateSamlCfgDTO = { @@ -36,7 +37,8 @@ export type TGetSamlCfgDTO = }; export type TSamlLoginDTO = { - email: string; + username: string; + email?: string; firstName: string; lastName?: string; authProvider: string; 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 0000000000..05c21b80c3 --- /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 0000000000..8b68870daa --- /dev/null +++ b/backend/src/ee/services/scim/scim-fns.ts @@ -0,0 +1,64 @@ +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, + username, + email, + firstName, + lastName, + active +}: { + userId: string; + username: string; + email?: string | null; + firstName: string; + lastName: string; + active: boolean; +}): TScimUser => { + const scimUser = { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], + id: userId, + userName: username, + displayName: `${firstName} ${lastName}`, + name: { + givenName: firstName, + middleName: null, + familyName: lastName + }, + emails: email + ? [ + { + primary: true, + value: email, + type: "work" + } + ] + : [], + active, + groups: [], + meta: { + resourceType: "User", + location: null + } + }; + + return scimUser; +}; 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 0000000000..c542b23401 --- /dev/null +++ b/backend/src/ee/services/scim/scim-service.ts @@ -0,0 +1,446 @@ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +import { OrgMembershipRole, OrgMembershipStatus, TableName } 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( + { + [`${TableName.OrgMembership}.orgId` as "id"]: orgId, + ...parseFilter(filter) + }, + findOpts + ); + + const scimUsers = users.map(({ userId, username, firstName, lastName, email }) => + buildScimUser({ + userId: userId ?? "", + username, + firstName: firstName ?? "", + lastName: lastName ?? "", + email, + active: true + }) + ); + + return buildScimUserList({ + scimUsers, + offset, + limit + }); + }; + + const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => { + const [membership] = await orgDAL + .findMembership({ + userId, + [`${TableName.OrgMembership}.orgId` as "id"]: 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, + username: membership.username, + email: membership.email ?? "", + firstName: membership.firstName as string, + lastName: membership.lastName as string, + active: true + }); + }; + + const createScimUser = async ({ username, email, firstName, lastName, 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({ + username + }); + + if (user) { + await userDAL.transaction(async (tx) => { + const [orgMembership] = await orgDAL.findMembership( + { + userId: user.id, + [`${TableName.OrgMembership}.orgId` as "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( + { + username, + 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(); + + if (email) { + 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, + username: user.username, + 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, + [`${TableName.OrgMembership}.orgId` as "id"]: 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, + username: membership.username, + email: membership.email, + firstName: membership.firstName as string, + lastName: membership.lastName as string, + active + }); + }; + + const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => { + const [membership] = await orgDAL + .findMembership({ + userId, + [`${TableName.OrgMembership}.orgId` as "id"]: 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, + username: membership.username, + email: membership.email, + firstName: membership.firstName as string, + lastName: membership.lastName as string, + 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 0000000000..c99dec794d --- /dev/null +++ b/backend/src/ee/services/scim/scim-types.ts @@ -0,0 +1,88 @@ +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 = { + username: string; + 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 9b4742255f..736cd253ea 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 ef10db8048..b48b6bf951 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,9 +11,12 @@ 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 { TSecretDALFactory } from "@app/services/secret/secret-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"; +import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; @@ -43,10 +46,13 @@ type TSecretApprovalRequestServiceFactoryDep = { secretApprovalRequestSecretDAL: TSecretApprovalRequestSecretDALFactory; secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory; folderDAL: Pick; - secretTagDAL: Pick; + secretDAL: TSecretDALFactory; + secretTagDAL: Pick; secretBlindIndexDAL: Pick; snapshotService: Pick; - secretVersionDAL: Pick; + secretVersionDAL: Pick; + secretVersionTagDAL: Pick; + projectDAL: Pick; secretService: Pick< TSecretServiceFactory, | "fnSecretBulkInsert" @@ -62,11 +68,14 @@ export type TSecretApprovalRequestServiceFactory = ReturnType approverId === membership.id) ) { @@ -147,14 +156,14 @@ export const secretApprovalRequestServiceFactory = ({ if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); const { policy } = secretApprovalRequest; - const { membership } = await permissionService.getProjectPermission( + const { membership, hasRole } = await permissionService.getProjectPermission( ActorType.USER, actorId, secretApprovalRequest.projectId, actorOrgId ); if ( - membership.role !== ProjectMembershipRole.Admin && + !hasRole(ProjectMembershipRole.Admin) && secretApprovalRequest.committerId !== membership.id && !policy.approvers.find((approverId) => approverId === membership.id) ) { @@ -189,14 +198,14 @@ export const secretApprovalRequestServiceFactory = ({ if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); const { policy } = secretApprovalRequest; - const { membership } = await permissionService.getProjectPermission( + const { membership, hasRole } = await permissionService.getProjectPermission( ActorType.USER, actorId, secretApprovalRequest.projectId, actorOrgId ); if ( - membership.role !== ProjectMembershipRole.Admin && + !hasRole(ProjectMembershipRole.Admin) && secretApprovalRequest.committerId !== membership.id && !policy.approvers.find((approverId) => approverId === membership.id) ) { @@ -227,9 +236,14 @@ export const secretApprovalRequestServiceFactory = ({ if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); const { policy, folderId, projectId } = secretApprovalRequest; - const { membership } = await permissionService.getProjectPermission(ActorType.USER, actorId, projectId, actorOrgId); + const { membership, hasRole } = await permissionService.getProjectPermission( + ActorType.USER, + actorId, + projectId, + actorOrgId + ); if ( - membership.role !== ProjectMembershipRole.Admin && + !hasRole(ProjectMembershipRole.Admin) && secretApprovalRequest.committerId !== membership.id && !policy.approvers.find((approverId) => approverId === membership.id) ) { @@ -332,7 +346,11 @@ export const secretApprovalRequestServiceFactory = ({ tags: el?.tags.map(({ id }) => id), version: 1, type: SecretType.Shared - })) + })), + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL }) : []; const updatedSecrets = secretUpdationCommits.length @@ -364,7 +382,11 @@ export const secretApprovalRequestServiceFactory = ({ "secretBlindIndex" ]) } - })) + })), + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL }) : []; const deletedSecret = secretDeletionCommits.length @@ -434,6 +456,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; @@ -450,7 +474,8 @@ export const secretApprovalRequestServiceFactory = ({ inputSecrets: createdSecrets, folderId, isNew: true, - blindIndexCfg + blindIndexCfg, + secretDAL }); commits.push( @@ -477,7 +502,8 @@ export const secretApprovalRequestServiceFactory = ({ inputSecrets: updatedSecrets, folderId, isNew: false, - blindIndexCfg + blindIndexCfg, + secretDAL }); // now find any secret that needs to update its name @@ -487,7 +513,8 @@ export const secretApprovalRequestServiceFactory = ({ inputSecrets: nameUpdatedSecrets, folderId, isNew: true, - blindIndexCfg + blindIndexCfg, + secretDAL }); const secsGroupedByBlindIndex = groupBy(secretsToBeUpdated, (el) => el.secretBlindIndex as string); @@ -526,7 +553,8 @@ export const secretApprovalRequestServiceFactory = ({ inputSecrets: deletedSecrets, folderId, isNew: false, - blindIndexCfg + blindIndexCfg, + secretDAL }); const secretsGroupedByBlindIndex = groupBy(secrets, (i) => { if (!i.secretBlindIndex) throw new BadRequestError({ message: "Missing secret blind index" }); diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 9e69f0a8fe..140a9b6710 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -1,3 +1,10 @@ +import { + CreateAccessKeyCommand, + DeleteAccessKeyCommand, + GetAccessKeyLastUsedCommand, + IAMClient +} from "@aws-sdk/client-iam"; + import { SecretKeyEncoding, SecretType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { @@ -18,7 +25,12 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { TSecretRotationDALFactory } from "../secret-rotation-dal"; import { rotationTemplates } from "../templates"; -import { TDbProviderClients, TProviderFunctionTypes, TSecretRotationProviderTemplate } from "../templates/types"; +import { + TAwsProviderSystems, + TDbProviderClients, + TProviderFunctionTypes, + TSecretRotationProviderTemplate +} from "../templates/types"; import { getDbSetQuery, secretRotationDbFn, @@ -127,7 +139,10 @@ export const secretRotationQueueFactory = ({ internal: {} }; - // when its a database we keep cycling the variables accordingly + /* Rotation Function For Database + * A database like sql cannot have multiple password for a user + * thus we ask users to create two users with required permission and then we keep cycling between these two db users + */ if (provider.template.type === TProviderFunctionTypes.DB) { const lastCred = variables.creds.at(-1); if (lastCred && variables.creds.length === 1) { @@ -170,6 +185,65 @@ export const secretRotationQueueFactory = ({ if (variables.creds.length === 2) variables.creds.pop(); } + /* + * Rotation Function For AWS Services + * Due to complexity in AWS Authorization hashing signature process we keep it as seperate entity instead of http template mode + * We first delete old key before creating a new one because aws iam has a quota limit of 2 keys + * */ + if (provider.template.type === TProviderFunctionTypes.AWS) { + if (provider.template.client === TAwsProviderSystems.IAM) { + const client = new IAMClient({ + region: newCredential.inputs.manager_user_aws_region as string, + credentials: { + accessKeyId: newCredential.inputs.manager_user_access_key as string, + secretAccessKey: newCredential.inputs.manager_user_secret_key as string + } + }); + + const iamUserName = newCredential.inputs.iam_username as string; + + if (variables.creds.length === 2) { + const deleteCycleCredential = variables.creds.pop(); + if (deleteCycleCredential) { + const deletedIamAccessKey = await client.send( + new DeleteAccessKeyCommand({ + UserName: iamUserName, + AccessKeyId: deleteCycleCredential.outputs.iam_user_access_key as string + }) + ); + + if ( + !deletedIamAccessKey?.$metadata?.httpStatusCode || + deletedIamAccessKey?.$metadata?.httpStatusCode > 300 + ) { + throw new DisableRotationErrors({ + message: "Failed to delete aws iam access key. Check managed iam user policy" + }); + } + } + } + + const newIamAccessKey = await client.send(new CreateAccessKeyCommand({ UserName: iamUserName })); + if (!newIamAccessKey.AccessKey) + throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" }); + + // test + const testAccessKey = await client.send( + new GetAccessKeyLastUsedCommand({ AccessKeyId: newIamAccessKey.AccessKey.AccessKeyId }) + ); + if (testAccessKey?.UserName !== iamUserName) + throw new DisableRotationErrors({ message: "Failed to create access key. Check managed iam user policy" }); + + newCredential.outputs.iam_user_access_key = newIamAccessKey.AccessKey.AccessKeyId; + newCredential.outputs.iam_user_secret_key = newIamAccessKey.AccessKey.SecretAccessKey; + } + } + + /* Rotation function of HTTP infisical template + * This is a generic http based template system for rotation + * we use this for sendgrid and for custom secret rotation + * This will ensure user provided rotation is easier to make + * */ if (provider.template.type === TProviderFunctionTypes.HTTP) { if (provider.template.functions.set?.pre) { secretRotationPreSetFn(provider.template.functions.set.pre, newCredential); @@ -185,6 +259,9 @@ export const secretRotationQueueFactory = ({ } } } + + // insert the new variables to start + // encrypt the data - save it variables.creds.unshift({ outputs: newCredential.outputs, internal: newCredential.internal @@ -200,6 +277,7 @@ export const secretRotationQueueFactory = ({ key ) })); + // map the final values to output keys in the board await secretRotationDAL.transaction(async (tx) => { await secretRotationDAL.updateById( rotationId, @@ -240,7 +318,7 @@ export const secretRotationQueueFactory = ({ ); }); - telemetryService.sendPostHogEvents({ + await telemetryService.sendPostHogEvents({ event: PostHogEventTypes.SecretRotated, distinctId: "", properties: { diff --git a/backend/src/ee/services/secret-rotation/templates/aws-iam.ts b/backend/src/ee/services/secret-rotation/templates/aws-iam.ts new file mode 100644 index 0000000000..d4506c26e9 --- /dev/null +++ b/backend/src/ee/services/secret-rotation/templates/aws-iam.ts @@ -0,0 +1,21 @@ +import { TAwsProviderSystems, TProviderFunctionTypes } from "./types"; + +export const AWS_IAM_TEMPLATE = { + type: TProviderFunctionTypes.AWS as const, + client: TAwsProviderSystems.IAM, + inputs: { + type: "object" as const, + properties: { + manager_user_access_key: { type: "string" as const }, + manager_user_secret_key: { type: "string" as const }, + manager_user_aws_region: { type: "string" as const }, + iam_username: { type: "string" as const } + }, + required: ["manager_user_access_key", "manager_user_secret_key", "manager_user_aws_region", "iam_username"], + additionalProperties: false + }, + outputs: { + iam_user_access_key: { type: "string" }, + iam_user_secret_key: { type: "string" } + } +}; diff --git a/backend/src/ee/services/secret-rotation/templates/index.ts b/backend/src/ee/services/secret-rotation/templates/index.ts index 3d9fb22984..05811d5bdb 100644 --- a/backend/src/ee/services/secret-rotation/templates/index.ts +++ b/backend/src/ee/services/secret-rotation/templates/index.ts @@ -1,3 +1,4 @@ +import { AWS_IAM_TEMPLATE } from "./aws-iam"; import { MYSQL_TEMPLATE } from "./mysql"; import { POSTGRES_TEMPLATE } from "./postgres"; import { SENDGRID_TEMPLATE } from "./sendgrid"; @@ -24,5 +25,12 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [ image: "mysql.png", description: "Rotate MySQL@7/MariaDB user credentials", template: MYSQL_TEMPLATE + }, + { + name: "aws-iam", + title: "AWS IAM", + image: "aws-iam.svg", + description: "Rotate AWS IAM User credentials", + template: AWS_IAM_TEMPLATE } ]; diff --git a/backend/src/ee/services/secret-rotation/templates/types.ts b/backend/src/ee/services/secret-rotation/templates/types.ts index cb48a7782b..690b6ccf02 100644 --- a/backend/src/ee/services/secret-rotation/templates/types.ts +++ b/backend/src/ee/services/secret-rotation/templates/types.ts @@ -1,6 +1,7 @@ export enum TProviderFunctionTypes { HTTP = "http", - DB = "database" + DB = "database", + AWS = "aws" } export enum TDbProviderClients { @@ -10,6 +11,10 @@ export enum TDbProviderClients { MySql = "mysql" } +export enum TAwsProviderSystems { + IAM = "iam" +} + export enum TAssignOp { Direct = "direct", JmesPath = "jmesopath" @@ -42,7 +47,7 @@ export type TSecretRotationProviderTemplate = { title: string; image?: string; description?: string; - template: THttpProviderTemplate | TDbProviderTemplate; + template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate; }; export type THttpProviderTemplate = { @@ -70,3 +75,14 @@ export type TDbProviderTemplate = { }; outputs: Record; }; + +export type TAwsProviderTemplate = { + type: TProviderFunctionTypes.AWS; + client: TAwsProviderSystems; + inputs: { + type: "object"; + properties: Record; + required?: string[]; + }; + outputs: Record; +}; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts index aab8d1218a..1b19fd7f55 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue.ts @@ -64,7 +64,7 @@ export const secretScanningQueueFactory = ({ orgId: organizationId, role: OrgMembershipRole.Admin }); - return adminsOfWork.map((userObject) => userObject.email); + return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string); }; queueService.start(QueueName.SecretPushEventScan, async (job) => { @@ -149,7 +149,7 @@ export const secretScanningQueueFactory = ({ await smtpService.sendMail({ template: SmtpTemplates.SecretLeakIncident, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, - recipients: adminEmails, + recipients: adminEmails.filter((email) => email).map((email) => email), substitutions: { numberOfSecrets: Object.keys(allFindingsByFingerprint).length, pusher_email: pusher.email, @@ -158,7 +158,7 @@ export const secretScanningQueueFactory = ({ }); } - telemetryService.sendPostHogEvents({ + await telemetryService.sendPostHogEvents({ event: PostHogEventTypes.SecretScannerPush, distinctId: repository.fullName, properties: { @@ -221,14 +221,14 @@ export const secretScanningQueueFactory = ({ await smtpService.sendMail({ template: SmtpTemplates.SecretLeakIncident, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`, - recipients: adminEmails, + recipients: adminEmails.filter((email) => email).map((email) => email), substitutions: { numberOfSecrets: findings.length } }); } - telemetryService.sendPostHogEvents({ + await telemetryService.sendPostHogEvents({ event: PostHogEventTypes.SecretScannerFull, distinctId: repository.fullName, properties: { diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts new file mode 100644 index 0000000000..5e2c3aab30 --- /dev/null +++ b/backend/src/keystore/keystore.ts @@ -0,0 +1,20 @@ +import { Redis } from "ioredis"; + +export type TKeyStoreFactory = ReturnType; + +export const keyStoreFactory = (redisUrl: string) => { + const redis = new Redis(redisUrl); + + const setItem = async (key: string, value: string | number | Buffer) => redis.set(key, value); + + const getItem = async (key: string) => redis.get(key); + + const setItemWithExpiry = async (key: string, exp: number | string, value: string | number | Buffer) => + redis.setex(key, exp, value); + + const deleteItem = async (key: string) => redis.del(key); + + const incrementBy = async (key: string, value: number) => redis.incrby(key, value); + + return { setItem, getItem, setItemWithExpiry, deleteItem, incrementBy }; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts new file mode 100644 index 0000000000..6f5cc22963 --- /dev/null +++ b/backend/src/lib/api-docs/constants.ts @@ -0,0 +1,286 @@ +export const IDENTITIES = { + CREATE: { + name: "The name of the identity to create.", + organizationId: "The organization ID to which the identity belongs.", + role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'." + }, + UPDATE: { + identityId: "The ID of the identity to update.", + name: "The new name of the identity.", + role: "The new role of the identity." + }, + DELETE: { + identityId: "The ID of the identity to delete." + } +} as const; + +export const UNIVERSAL_AUTH = { + LOGIN: { + clientId: "Your Machine Identity Client ID.", + clientSecret: "Your Machine Identity Client Secret." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + clientSecretTrustedIps: + "A list of IPs or CIDR ranges that the Client Secret can be used from together with the Client ID to get back an access token. You can use 0.0.0.0/0, to allow usage from any network address.", + accessTokenTrustedIps: + "A list of IPs or CIDR ranges that access tokens can be used from. You can use 0.0.0.0/0, to allow usage from any network address.", + accessTokenTTL: "The lifetime for an access token in seconds. This value will be referenced at renewal time.", + accessTokenMaxTTL: + "The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.", + accessTokenNumUsesLimit: + "The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve." + }, + UPDATE: { + identityId: "The ID of the identity to update.", + clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.", + accessTokenTrustedIps: "The new list of IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The new lifetime for an access token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." + }, + CREATE_CLIENT_SECRET: { + identityId: "The ID of the identity to create a client secret for.", + description: "The description of the client secret.", + numUsesLimit: + "The maximum number of times that the client secret can be used; a value of 0 implies infinite number of uses.", + ttl: "The lifetime for the client secret in seconds." + }, + LIST_CLIENT_SECRETS: { + identityId: "The ID of the identity to list client secrets for." + }, + REVOKE_CLIENT_SECRET: { + identityId: "The ID of the identity to revoke the client secret from.", + clientSecretId: "The ID of the client secret to revoke." + }, + RENEW_ACCESS_TOKEN: { + accessToken: "The access token to renew." + } +} as const; + +export const ORGANIZATIONS = { + LIST_USER_MEMBERSHIPS: { + organizationId: "The ID of the organization to get memberships from." + }, + UPDATE_USER_MEMBERSHIP: { + organizationId: "The ID of the organization to update the membership for.", + membershipId: "The ID of the membership to update.", + role: "The new role of the membership." + }, + DELETE_USER_MEMBERSHIP: { + organizationId: "The ID of the organization to delete the membership from.", + membershipId: "The ID of the membership to delete." + }, + LIST_IDENTITY_MEMBERSHIPS: { + orgId: "The ID of the organization to get identity memberships from." + }, + GET_PROJECTS: { + organizationId: "The ID of the organization to get projects from." + } +} as const; + +export const PROJECTS = { + CREATE: { + organizationId: "The ID of the organization to create the project in.", + projectName: "The name of the project to create.", + slug: "An optional slug for the project." + }, + DELETE: { + workspaceId: "The ID of the project to delete." + }, + GET: { + workspaceId: "The ID of the project." + }, + UPDATE: { + workspaceId: "The ID of the project to update.", + name: "The new name of the project.", + autoCapitalization: "Disable or enable auto-capitalization for the project." + }, + INVITE_MEMBER: { + projectId: "The ID of the project to invite the member to.", + emails: "A list of organization member emails to invite to the project.", + usernames: "A list of usernames to invite to the project." + }, + REMOVE_MEMBER: { + projectId: "The ID of the project to remove the member from.", + emails: "A list of organization member emails to remove from the project.", + usernames: "A list of usernames to remove from the project." + }, + GET_USER_MEMBERSHIPS: { + workspaceId: "The ID of the project to get memberships from." + }, + UPDATE_USER_MEMBERSHIP: { + workspaceId: "The ID of the project to update the membership for.", + membershipId: "The ID of the membership to update.", + roles: "A list of roles to update the membership to." + }, + LIST_IDENTITY_MEMBERSHIPS: { + projectId: "The ID of the project to get identity memberships from." + }, + UPDATE_IDENTITY_MEMBERSHIP: { + projectId: "The ID of the project to update the identity membership for.", + identityId: "The ID of the identity to update the membership for.", + roles: "A list of roles to update the membership to." + }, + DELETE_IDENTITY_MEMBERSHIP: { + projectId: "The ID of the project to delete the identity membership from.", + identityId: "The ID of the identity to delete the membership from." + }, + GET_KEY: { + workspaceId: "The ID of the project to get the key from." + }, + GET_SNAPSHOTS: { + workspaceId: "The ID of the project to get snapshots from.", + environment: "The environment to get snapshots from.", + path: "The secret path to get snapshots from.", + offset: "The offset to start from. If you enter 10, it will start from the 10th snapshot.", + limit: "The number of snapshots to return." + }, + ROLLBACK_TO_SNAPSHOT: { + secretSnapshotId: "The ID of the snapshot to rollback to." + } +} as const; + +export const ENVIRONMENTS = { + CREATE: { + workspaceId: "The ID of the project to create the environment in.", + name: "The name of the environment to create.", + slug: "The slug of the environment to create." + }, + UPDATE: { + workspaceId: "The ID of the project to update the environment in.", + id: "The ID of the environment to update.", + name: "The new name of the environment.", + slug: "The new slug of the environment.", + position: "The new position of the environment. The lowest number will be displayed as the first environment." + }, + DELETE: { + workspaceId: "The ID of the project to delete the environment from.", + id: "The ID of the environment to delete." + } +} as const; + +export const FOLDERS = { + LIST: { + workspaceId: "The ID of the project to list folders from.", + environment: "The slug of the environment to list folders from.", + path: "The path to list folders from.", + directory: "The directory to list folders from. (Deprecated in favor of path)" + }, + CREATE: { + workspaceId: "The ID of the project to create the folder in.", + environment: "The slug of the environment to create the folder in.", + name: "The name of the folder to create.", + path: "The path of the folder to create.", + directory: "The directory of the folder to create. (Deprecated in favor of path)" + }, + UPDATE: { + folderId: "The ID of the folder to update.", + environment: "The slug of the environment where the folder is located.", + name: "The new name of the folder.", + path: "The path of the folder to update.", + directory: "The new directory of the folder to update. (Deprecated in favor of path)", + workspaceId: "The ID of the project where the folder is located." + }, + DELETE: { + folderIdOrName: "The ID or name of the folder to delete.", + workspaceId: "The ID of the project to delete the folder from.", + environment: "The slug of the environment where the folder is located.", + directory: "The directory of the folder to delete. (Deprecated in favor of path)", + path: "The path of the folder to delete." + } +} as const; + +export const RAW_SECRETS = { + LIST: { + workspaceId: "The ID of the project to list secrets from.", + environment: "The slug of the environment to list secrets from.", + secretPath: "The secret path to list secrets from.", + includeImports: "Weather to include imported secrets or not." + }, + CREATE: { + secretName: "The name of the secret to create.", + environment: "The slug of the environment to create the secret in.", + secretComment: "Attach a comment to the secret.", + secretPath: "The path to create the secret in.", + secretValue: "The value of the secret to create.", + skipMultilineEncoding: "Skip multiline encoding for the secret value.", + type: "The type of the secret to create.", + workspaceId: "The ID of the project to create the secret in." + }, + GET: { + secretName: "The name of the secret to get.", + workspaceId: "The ID of the project to get the secret from.", + environment: "The slug of the environment to get the secret from.", + secretPath: "The path of the secret to get.", + version: "The version of the secret to get.", + type: "The type of the secret to get.", + includeImports: "Weather to include imported secrets or not." + }, + UPDATE: { + secretName: "The name of the secret to update.", + environment: "The slug of the environment where the secret is located.", + secretPath: "The path of the secret to update", + secretValue: "The new value of the secret.", + skipMultilineEncoding: "Skip multiline encoding for the secret value.", + type: "The type of the secret to update.", + workspaceId: "The ID of the project to update the secret in." + }, + DELETE: { + secretName: "The name of the secret to delete.", + environment: "The slug of the environment where the secret is located.", + secretPath: "The path of the secret.", + type: "The type of the secret to delete.", + workspaceId: "The ID of the project where the secret is located." + } +} as const; + +export const SECRET_IMPORTS = { + LIST: { + workspaceId: "The ID of the project to list secret imports from.", + environment: "The slug of the environment to list secret imports from.", + path: "The path to list secret imports from." + }, + CREATE: { + environment: "The slug of the environment to import into.", + path: "The path to import into.", + workspaceId: "The ID of the project you are working in.", + import: { + environment: "The slug of the environment to import from.", + path: "The path to import from." + } + }, + UPDATE: { + secretImportId: "The ID of the secret import to update.", + environment: "The slug of the environment where the secret import is located.", + import: { + environment: "The new environment slug to import from.", + path: "The new path to import from.", + position: "The new position of the secret import. The lowest number will be displayed as the first import." + }, + path: "The path of the secret import to update.", + workspaceId: "The ID of the project where the secret import is located." + }, + DELETE: { + workspaceId: "The ID of the project to delete the secret import from.", + secretImportId: "The ID of the secret import to delete.", + environment: "The slug of the environment where the secret import is located.", + path: "The path of the secret import to delete." + } +} as const; + +export const AUDIT_LOGS = { + EXPORT: { + workspaceId: "The ID of the project to export audit logs from.", + eventType: "The type of the event to export.", + userAgentType: "Choose which consuming application to export audit logs for.", + startDate: "The date to start the export from.", + endDate: "The date to end the export at.", + offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.", + limit: "The number of audit logs to return.", + actor: "The actor to filter the audit logs by." + } +} as const; diff --git a/backend/src/lib/api-docs/index.ts b/backend/src/lib/api-docs/index.ts new file mode 100644 index 0000000000..b04bfcf75e --- /dev/null +++ b/backend/src/lib/api-docs/index.ts @@ -0,0 +1 @@ +export * from "./constants"; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 4542c7fc37..6b7c02f6bc 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -15,8 +15,16 @@ const envSchema = z PORT: z.coerce.number().default(4000), REDIS_URL: zpStr(z.string()), HOST: zpStr(z.string().default("localhost")), - DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")), + DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( + `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` + ), DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), + DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), + DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"), + DB_USER: zpStr(z.string().describe("Postgres database username").optional()), + DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()), + DB_NAME: zpStr(z.string().describe("Postgres database name").optional()), + NODE_ENV: z.enum(["development", "test", "production"]).default("production"), SALT_ROUNDS: z.coerce.number().default(10), INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()), @@ -94,14 +102,18 @@ const envSchema = z SECRET_SCANNING_WEBHOOK_SECRET: zpStr(z.string().optional()), SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()), SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()), - // LICENCE + // LICENSE LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")), LICENSE_SERVER_KEY: zpStr(z.string().optional()), LICENSE_KEY: zpStr(z.string().optional()), + LICENSE_KEY_OFFLINE: zpStr(z.string().optional()), + + // GENERIC STANDALONE_MODE: z .enum(["true", "false"]) .transform((val) => val === "true") - .optional() + .optional(), + INFISICAL_CLOUD: zodStrBool.default("false") }) .transform((data) => ({ ...data, diff --git a/backend/src/lib/crypto/encryption.ts b/backend/src/lib/crypto/encryption.ts index 74febccec8..16a7f42e7c 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 62278de1d5..db3d91fc81 100644 --- a/backend/src/lib/crypto/index.ts +++ b/backend/src/lib/crypto/index.ts @@ -1,12 +1,21 @@ export { buildSecretBlindIndexFromName, createSecretBlindIndex, + decodeBase64, decryptAsymmetric, decryptSymmetric, decryptSymmetric128BitHexKeyUTF8, + encodeBase64, encryptAsymmetric, encryptSymmetric, encryptSymmetric128BitHexKeyUTF8, generateAsymmetricKeyPair } from "./encryption"; +export { + decryptIntegrationAuths, + decryptSecretApprovals, + decryptSecrets, + decryptSecretVersions +} from "./secret-encryption"; +export { verifyOfflineLicense } from "./signing"; export { generateSrpServerKey, srpCheckClientProof } from "./srp"; diff --git a/backend/src/lib/crypto/license_public_key.pem b/backend/src/lib/crypto/license_public_key.pem new file mode 100644 index 0000000000..0cda06f3cf --- /dev/null +++ b/backend/src/lib/crypto/license_public_key.pem @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEApchBY3BXTu4zWGBguB7nM/pjpVLY3V7VGZOAxmR5ueQTJOwiGM13 +5HN3EM9fDlQnZu9VSc0OFqRM/bUeUaI1oLPE6WzTHjdHyKjDI/S+TLx3VGEsvhM1 +uukZpYX+3KX2w4wzRHBaBWyglFy0CVNth9UJhhpD+KKfv7dzcRmsbyoUWi9wGfJu +wLYCwaCwZRXIt1sLGmMncPz14vfwdnm2a5Tj1Jbt0GTyBl+1/ZqLbO6SsslLg2G+ +o7FfGS9z8OUTkvDdu16qxL+p2wCEFZMnOz5BB4oakuT2gS9iOO2l5AOPcT4WzPzy +PYbX3d7cN9BkOY9I5z0cX4wzqHjQTvGNLQIDAQAB +-----END RSA PUBLIC KEY----- \ No newline at end of file diff --git a/backend/src/lib/crypto/secret-encryption.ts b/backend/src/lib/crypto/secret-encryption.ts new file mode 100644 index 0000000000..2e04925608 --- /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/signing.ts b/backend/src/lib/crypto/signing.ts new file mode 100644 index 0000000000..36c8587153 --- /dev/null +++ b/backend/src/lib/crypto/signing.ts @@ -0,0 +1,22 @@ +import crypto, { KeyObject } from "crypto"; +import fs from "fs/promises"; +import path from "path"; + +export const verifySignature = (data: string, signature: Buffer, publicKey: KeyObject) => { + const verify = crypto.createVerify("SHA256"); + verify.update(data); + verify.end(); + return verify.verify(publicKey, signature); +}; + +export const verifyOfflineLicense = async (licenseContents: string, signature: string) => { + const publicKeyPem = await fs.readFile(path.join(__dirname, "license_public_key.pem"), "utf8"); + + const publicKey = crypto.createPublicKey({ + key: publicKeyPem, + format: "pem", + type: "pkcs1" + }); + + return verifySignature(licenseContents, Buffer.from(signature, "base64"), publicKey); +}; diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index b05f3734d5..bc29cdb3f7 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 b4376f007d..d93244bbda 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/main.ts b/backend/src/main.ts index fab576d3bd..86681ef337 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,6 +1,7 @@ import dotenv from "dotenv"; import { initDbConnection } from "./db"; +import { keyStoreFactory } from "./keystore/keystore"; import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; @@ -19,8 +20,9 @@ const run = async () => { const smtp = smtpServiceFactory(formatSmtpConfig()); const queue = queueServiceFactory(appCfg.REDIS_URL); + const keyStore = keyStoreFactory(appCfg.REDIS_URL); - const server = await main({ db, smtp, logger, queue }); + const server = await main({ db, smtp, logger, queue, keyStore }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line process.on("SIGINT", async () => { diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 58c829549c..45c135b77a 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, @@ -12,10 +13,12 @@ export enum QueueName { SecretReminder = "secret-reminder", AuditLog = "audit-log", AuditLogPrune = "audit-log-prune", + TelemetryInstanceStats = "telemtry-self-hosted-stats", 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 { @@ -24,8 +27,10 @@ export enum QueueJobs { AuditLog = "audit-log-job", AuditLogPrune = "audit-log-prune-job", SecWebhook = "secret-webhook-trigger", + TelemetryInstanceStats = "telemetry-self-hosted-stats", IntegrationSync = "secret-integration-pull", - SecretScan = "secret-scan" + SecretScan = "secret-scan", + UpgradeProjectToGhost = "upgrade-project-to-ghost-job" } export type TQueueJobTypes = { @@ -64,6 +69,23 @@ 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; + }; + }; + }; + [QueueName.TelemetryInstanceStats]: { + name: QueueJobs.TelemetryInstanceStats; + payload: undefined; + }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index ca6b9003a1..556a88d7cc 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -14,6 +14,7 @@ import fasitfy from "fastify"; import { Knex } from "knex"; import { Logger } from "pino"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; import { TSmtpService } from "@app/services/smtp/smtp-service"; @@ -31,13 +32,14 @@ type TMain = { smtp: TSmtpService; logger?: Logger; queue: TQueueServiceFactory; + keyStore: TKeyStoreFactory; }; // Run the server! -export const main = async ({ db, smtp, logger, queue }: TMain) => { +export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => { const appCfg = getConfig(); const server = fasitfy({ - logger, + logger: appCfg.NODE_ENV === "test" ? false : logger, trustProxy: true, connectionTimeout: 30 * 1000, ignoreTrailingSlash: true @@ -70,7 +72,7 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => { } await server.register(helmet, { contentSecurityPolicy: false }); - await server.register(registerRoutes, { smtp, queue, db }); + await server.register(registerRoutes, { smtp, queue, db, keyStore }); if (appCfg.isProductionMode) { await server.register(registerExternalNextjs, { diff --git a/backend/src/server/lib/telemetry.ts b/backend/src/server/lib/telemetry.ts new file mode 100644 index 0000000000..9d04d0357e --- /dev/null +++ b/backend/src/server/lib/telemetry.ts @@ -0,0 +1,17 @@ +import { FastifyRequest } from "fastify"; + +import { ActorType } from "@app/services/auth/auth-type"; + +// this is a unique id for sending posthog event +export const getTelemetryDistinctId = (req: FastifyRequest) => { + if (req.auth.actor === ActorType.USER) { + return req.auth.user.username; + } + if (req.auth.actor === ActorType.IDENTITY) { + return `identity-${req.auth.identityId}`; + } + if (req.auth.actor === ActorType.SERVICE) { + return req.auth.serviceToken.createdByEmail || `service-token-null-creator-${req.auth.serviceTokenId}`; // when user gets removed from system + } + return "unknown-auth-data"; +}; diff --git a/backend/src/server/plugins/audit-log.ts b/backend/src/server/plugins/audit-log.ts index b42cc2ff22..084b0cb54e 100644 --- a/backend/src/server/plugins/audit-log.ts +++ b/backend/src/server/plugins/audit-log.ts @@ -44,6 +44,7 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => { type: ActorType.USER, metadata: { email: req.auth.user.email, + username: req.auth.user.username, userId: req.permission.id } }; @@ -63,6 +64,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 7abcd073c0..3a0a0ab39f 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"; @@ -26,7 +27,7 @@ export type TAuthMode = } | { authMode: AuthMode.SERVICE_TOKEN; - serviceToken: TServiceTokens; + serviceToken: TServiceTokens & { createdByEmail: string }; actor: ActorType.SERVICE; serviceTokenId: string; } @@ -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 572814d645..2d61647e8e 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 8587c93bd8..c8da4077af 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 28f65ea998..0a49806bd7 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -5,12 +5,16 @@ import { registerV1EERoutes } from "@app/ee/routes/v1"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; +import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; +import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { licenseDALFactory } from "@app/ee/services/license/license-dal"; import { licenseServiceFactory } from "@app/ee/services/license/license-service"; 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"; @@ -32,6 +36,7 @@ import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snaps import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; @@ -48,6 +53,7 @@ import { identityServiceFactory } from "@app/services/identity/identity-service" import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal"; import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal"; @@ -63,6 +69,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"; @@ -72,6 +79,7 @@ import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal" import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; +import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { secretDALFactory } from "@app/services/secret/secret-dal"; @@ -93,9 +101,12 @@ import { serviceTokenServiceFactory } from "@app/services/service-token/service- import { TSmtpService } from "@app/services/smtp/smtp-service"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; +import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal"; +import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-queue"; import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { userDALFactory } from "@app/services/user/user-dal"; import { userServiceFactory } from "@app/services/user/user-service"; +import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; @@ -109,12 +120,18 @@ import { registerV3Routes } from "./v3"; export const registerRoutes = async ( server: FastifyZodProvider, - { db, smtp: smtpService, queue: queueService }: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory } + { + db, + smtp: smtpService, + queue: queueService, + keyStore + }: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory } ) => { await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" }); // db layers const userDAL = userDALFactory(db); + const userAliasDAL = userAliasDALFactory(db); const authDAL = authDALFactory(db); const authTokenDAL = tokenDALFactory(db); const orgDAL = orgDALFactory(db); @@ -126,6 +143,7 @@ export const registerRoutes = async ( const projectDAL = projectDALFactory(db); const projectMembershipDAL = projectMembershipDALFactory(db); + const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db); const projectRoleDAL = projectRoleDALFactory(db); const projectEnvDAL = projectEnvDALFactory(db); const projectKeyDAL = projectKeyDALFactory(db); @@ -149,16 +167,20 @@ export const registerRoutes = async ( const identityAccessTokenDAL = identityAccessTokenDALFactory(db); const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db); + const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); const identityUaDAL = identityUaDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const auditLogDAL = auditLogDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db); + const telemetryDAL = telemetryDALFactory(db); // ee db layer ops const permissionDAL = permissionDALFactory(db); const samlConfigDAL = samlConfigDALFactory(db); + const scimDAL = scimDALFactory(db); + const ldapConfigDAL = ldapConfigDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); @@ -181,13 +203,14 @@ export const registerRoutes = async ( projectRoleDAL, serviceTokenDAL }); - const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL }); + const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const trustedIpService = trustedIpServiceFactory({ licenseService, projectDAL, trustedIpDAL, permissionService }); + const auditLogQueue = auditLogQueueServiceFactory({ auditLogDAL, queueService, @@ -210,8 +233,37 @@ export const registerRoutes = async ( samlConfigDAL, licenseService }); + const scimService = scimServiceFactory({ + licenseService, + scimDAL, + userDAL, + orgDAL, + projectDAL, + projectMembershipDAL, + permissionService, + smtpService + }); + + const ldapService = ldapConfigServiceFactory({ + ldapConfigDAL, + orgDAL, + orgBotDAL, + userDAL, + userAliasDAL, + permissionService, + licenseService + }); + + const telemetryService = telemetryServiceFactory({ + keyStore, + licenseService + }); + const telemetryQueue = telemetryQueueServiceFactory({ + keyStore, + telemetryDAL, + queueService + }); - const telemetryService = telemetryServiceFactory(); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); const userService = userServiceFactory({ userDAL }); const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService }); @@ -230,6 +282,8 @@ export const registerRoutes = async ( incidentContactDAL, tokenService, projectDAL, + projectMembershipDAL, + projectKeyDAL, smtpService, userDAL, orgBotDAL @@ -248,7 +302,8 @@ export const registerRoutes = async ( userDAL, authService: loginService, serverCfgDAL: superAdminDAL, - orgService + orgService, + keyStore }); const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL }); @@ -266,19 +321,14 @@ 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, + projectUserMembershipRoleDAL, projectDAL, permissionService, + projectBotDAL, orgDAL, userDAL, smtpService, @@ -286,6 +336,50 @@ 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, + projectUserMembershipRoleDAL + }); + + const projectService = projectServiceFactory({ + permissionService, + projectDAL, + projectQueue: projectQueueService, + secretBlindIndexDAL, + identityProjectDAL, + identityOrgMembershipDAL, + projectBotDAL, + projectKeyDAL, + userDAL, + projectEnvDAL, + orgService, + projectMembershipDAL, + folderDAL, + licenseService, + projectUserMembershipRoleDAL, + identityProjectMembershipRoleDAL + }); + const projectEnvService = projectEnvServiceFactory({ permissionService, projectEnvDAL, @@ -293,11 +387,7 @@ export const registerRoutes = async ( projectDAL, folderDAL }); - const projectKeyService = projectKeyServiceFactory({ - permissionService, - projectKeyDAL, - projectMembershipDAL - }); + const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL }); const snapshotService = secretSnapshotServiceFactory({ @@ -332,9 +422,9 @@ export const registerRoutes = async ( folderDAL, permissionService, secretImportDAL, + projectDAL, secretDAL }); - const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL }); const integrationAuthService = integrationAuthServiceFactory({ integrationAuthDAL, integrationDAL, @@ -355,7 +445,12 @@ export const registerRoutes = async ( orgDAL, projectMembershipDAL, smtpService, - projectDAL + projectDAL, + projectBotDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL }); const secretBlindIndexService = secretBlindIndexServiceFactory({ permissionService, @@ -368,6 +463,7 @@ export const registerRoutes = async ( secretVersionTagDAL, secretBlindIndexDAL, permissionService, + projectDAL, secretDAL, secretTagDAL, snapshotService, @@ -378,14 +474,17 @@ export const registerRoutes = async ( const sarService = secretApprovalRequestServiceFactory({ permissionService, folderDAL, + secretDAL, secretTagDAL, secretApprovalRequestSecretDAL: sarSecretDAL, secretApprovalRequestReviewerDAL: sarReviewerDAL, + projectDAL, secretVersionDAL, secretBlindIndexDAL, secretApprovalRequestDAL, secretService, snapshotService, + secretVersionTagDAL, secretQueueService }); const secretRotationQueue = secretRotationQueueFactory({ @@ -431,7 +530,9 @@ export const registerRoutes = async ( permissionService, projectDAL, identityProjectDAL, - identityOrgMembershipDAL + identityOrgMembershipDAL, + identityProjectMembershipRoleDAL, + projectRoleDAL }); const identityUaService = identityUaServiceFactory({ identityOrgMembershipDAL, @@ -444,9 +545,13 @@ export const registerRoutes = async ( }); await superAdminService.initServerCfg(); - await auditLogQueue.startAuditLogPruneJob(); + // // setup the communication with license key server await licenseService.init(); + + await auditLogQueue.startAuditLogPruneJob(); + await telemetryQueue.startTelemetryCheck(); + // inject all services server.decorate("services", { login: loginService, @@ -482,10 +587,12 @@ export const registerRoutes = async ( secretRotation: secretRotationService, snapshot: snapshotService, saml: samlService, + ldap: ldapService, auditLog: auditLogService, secretScanning: secretScanningService, license: licenseService, trustedIp: trustedIpService, + scim: scimService, secretBlindIndex: secretBlindIndexService, telemetry: telemetryService }); @@ -537,4 +644,8 @@ export const registerRoutes = async ( ); await server.register(registerV2Routes, { prefix: "/api/v2" }); await server.register(registerV3Routes, { prefix: "/api/v3" }); + + server.addHook("onClose", async () => { + await telemetryService.flushAll(); + }); }; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index da13d5e00c..1a048e9453 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"; @@ -16,7 +16,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - config: SuperAdminSchema + config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }) }) } }, @@ -31,7 +31,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { method: "PATCH", schema: { body: z.object({ - allowSignUp: z.boolean().optional() + allowSignUp: z.boolean().optional(), + allowedSignUpDomain: z.string().optional().nullable() }), response: { 200: z.object({ @@ -72,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() }) @@ -82,17 +84,18 @@ 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"] || "" }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.AdminInit, - distinctId: user.user.email, + distinctId: user.user.username ?? "", properties: { - email: user.user.email, + username: user.user.username, + email: user.user.email ?? "", lastName: user.user.lastName || "", firstName: user.user.firstName || "" } @@ -109,6 +112,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-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 78112f8962..5cd0b27a16 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; + export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvider) => { server.route({ url: "/token/renew", @@ -7,7 +9,7 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid schema: { description: "Renew access token", body: z.object({ - accessToken: z.string().trim() + accessToken: z.string().trim().describe(UNIVERSAL_AUTH.RENEW_ACCESS_TOKEN.accessToken) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 4ca4ef3249..ac389b4784 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -2,14 +2,17 @@ import { z } from "zod"; import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { IDENTITIES } from "@app/lib/api-docs"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; 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: [ @@ -18,9 +21,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { } ], body: z.object({ - name: z.string().trim(), - organizationId: z.string().trim(), - role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess) + name: z.string().trim().describe(IDENTITIES.CREATE.name), + organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId), + role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role) }), response: { 200: z.object({ @@ -49,6 +52,17 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { } }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.MachineIdentityCreated, + distinctId: getTelemetryDistinctId(req), + properties: { + orgId: req.body.organizationId, + name: identity.name, + identityId: identity.id, + ...req.auditLogInfo + } + }); + return { identity }; } }); @@ -56,7 +70,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: [ @@ -65,11 +79,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(IDENTITIES.UPDATE.identityId) }), body: z.object({ - name: z.string().trim().optional(), - role: z.string().trim().min(1).optional() + name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name), + role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role) }), response: { 200: z.object({ @@ -105,7 +119,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: [ @@ -114,7 +128,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(IDENTITIES.DELETE.identityId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-ua.ts b/backend/src/server/routes/v1/identity-ua.ts index 11b8e0e8de..6146fa2422 100644 --- a/backend/src/server/routes/v1/identity-ua.ts +++ b/backend/src/server/routes/v1/identity-ua.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -26,8 +27,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { schema: { description: "Login with Universal Auth", body: z.object({ - clientId: z.string().trim(), - clientSecret: z.string().trim() + clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId), + clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret) }), response: { 200: z.object({ @@ -39,11 +40,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { identityUa, accessToken, identityAccessToken, validClientSecretInfo } = + const { identityUa, accessToken, identityAccessToken, validClientSecretInfo, identityMembershipOrg } = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, event: { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, metadata: { @@ -75,7 +77,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(UNIVERSAL_AUTH.ATTACH.identityId) }), body: z.object({ clientSecretTrustedIps: z @@ -84,14 +86,16 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }) .array() .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() }) .array() .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps), accessTokenTTL: z .number() .int() @@ -99,15 +103,22 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { .refine((value) => value !== 0, { message: "accessTokenTTL must have a non zero number" }) - .default(2592000), + .default(2592000) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days accessTokenMaxTTL: z .number() .int() .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) - .default(2592000), // 30 days - accessTokenNumUsesLimit: z.number().int().min(0).default(0) + .default(2592000) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit) }), response: { 200: z.object({ @@ -155,7 +166,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(UNIVERSAL_AUTH.UPDATE.identityId) }), body: z.object({ clientSecretTrustedIps: z @@ -164,16 +175,23 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { }) .array() .min(1) - .optional(), + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() }) .array() .min(1) - .optional(), - accessTokenTTL: z.number().int().min(0).optional(), - accessTokenNumUsesLimit: z.number().int().min(0).optional(), + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).optional().describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit), accessTokenMaxTTL: z .number() .int() @@ -181,6 +199,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { message: "accessTokenMaxTTL must have a non zero number" }) .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL) }), response: { 200: z.object({ @@ -229,7 +248,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(UNIVERSAL_AUTH.RETRIEVE.identityId) }), response: { 200: z.object({ @@ -272,12 +291,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.identityId) }), body: z.object({ - description: z.string().trim().default(""), - numUsesLimit: z.number().min(0).default(0), - ttl: z.number().min(0).default(0) + description: z.string().trim().default("").describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.description), + numUsesLimit: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.numUsesLimit), + ttl: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.ttl) }), response: { 200: z.object({ @@ -323,7 +342,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(UNIVERSAL_AUTH.LIST_CLIENT_SECRETS.identityId) }), response: { 200: z.object({ @@ -365,8 +384,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - identityId: z.string(), - clientSecretId: z.string() + identityId: z.string().describe(UNIVERSAL_AUTH.REVOKE_CLIENT_SECRET.identityId), + clientSecretId: z.string().describe(UNIVERSAL_AUTH.REVOKE_CLIENT_SECRET.clientSecretId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 744ba6ebff..fbc68d974a 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/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 0decaf119f..7dfda4bee9 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -573,6 +573,37 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + url: "/:integrationAuthId/heroku/pipelines", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + pipelines: z + .object({ + app: z.object({ appId: z.string() }), + stage: z.string(), + pipeline: z.object({ name: z.string(), pipelineId: z.string() }) + }) + .array() + }) + } + }, + handler: async (req) => { + const pipelines = await server.services.integrationAuth.getHerokuPipelines({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId + }); + return { pipelines }; + } + }); + server.route({ url: "/:integrationAuthId/railway/environments", method: "GET", diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index ab0ba36eb9..ed1914ccc0 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -3,8 +3,10 @@ import { z } from "zod"; import { IntegrationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { removeTrailingSlash, shake } from "@app/lib/fn"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; export const registerIntegrationRouter = async (server: FastifyZodProvider) => { server.route({ @@ -30,6 +32,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { .object({ secretPrefix: z.string().optional(), secretSuffix: z.string().optional(), + initialSyncBehavior: z.string().optional(), secretGCPLabel: z .object({ labelName: z.string(), @@ -53,28 +56,40 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, ...req.body }); + + const createIntegrationEventProperty = shake({ + integrationId: integration.id.toString(), + integration: integration.integration, + environment: req.body.sourceEnvironment, + secretPath: req.body.secretPath, + url: integration.url, + app: integration.app, + appId: integration.appId, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId, + targetService: integration.targetService, + targetServiceId: integration.targetServiceId, + path: integration.path, + region: integration.region + }) as TIntegrationCreatedEvent["properties"]; + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: integrationAuth.projectId, event: { type: EventType.CREATE_INTEGRATION, // eslint-disable-next-line - metadata: shake({ - integrationId: integration.id.toString(), - integration: integration.integration, - environment: req.body.sourceEnvironment, - secretPath: req.body.secretPath, - url: integration.url, - app: integration.app, - appId: integration.appId, - targetEnvironment: integration.targetEnvironment, - targetEnvironmentId: integration.targetEnvironmentId, - targetService: integration.targetService, - targetServiceId: integration.targetServiceId, - path: integration.path, - region: integration.region - // eslint-disable-next-line - }) as any + metadata: createIntegrationEventProperty + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IntegrationCreated, + distinctId: getTelemetryDistinctId(req), + properties: { + ...createIntegrationEventProperty, + projectId: integrationAuth.projectId, + ...req.auditLogInfo } }); return { integration }; diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 0d1fe5070c..5956b53df9 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -1,8 +1,10 @@ import { z } from "zod"; import { UsersSchema } from "@app/db/schemas"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { server.route({ @@ -30,6 +32,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.UserOrgInvitation, + distinctId: getTelemetryDistinctId(req), + properties: { + inviteeEmail: req.body.inviteeEmail, + ...req.auditLogInfo + } + }); + return { completeInviteLink, message: `Send an invite link to ${req.body.inviteeEmail}` diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index bfda652f0c..d31682d88e 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -58,6 +58,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { users: OrgMembershipsSchema.merge( z.object({ user: UsersSchema.pick({ + username: true, email: true, firstName: true, lastName: true, @@ -87,13 +88,15 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { schema: { params: z.object({ organizationId: z.string().trim() }), body: z.object({ - name: z.string().trim().optional(), + name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(), slug: z .string() .trim() - .regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens") + .max(64, { message: "Slug must be 64 or fewer characters" }) + .regex(/^[a-zA-Z0-9-]+$/, "Slug 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-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index b93ffe9283..cb5be173e7 100644 --- a/backend/src/server/routes/v1/project-env-router.ts +++ b/backend/src/server/routes/v1/project-env-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { ProjectEnvironmentsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ENVIRONMENTS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,11 +19,11 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(ENVIRONMENTS.CREATE.workspaceId) }), body: z.object({ - name: z.string().trim(), - slug: z.string().trim() + name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), + slug: z.string().trim().describe(ENVIRONMENTS.CREATE.slug) }), response: { 200: z.object({ @@ -73,13 +74,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim(), - id: z.string().trim() + workspaceId: z.string().trim().describe(ENVIRONMENTS.UPDATE.workspaceId), + id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id) }), body: z.object({ - slug: z.string().trim().optional(), - name: z.string().trim().optional(), - position: z.number().optional() + slug: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.slug), + name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name), + position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position) }), response: { 200: z.object({ @@ -136,8 +137,8 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim(), - id: z.string().trim() + workspaceId: z.string().trim().describe(ENVIRONMENTS.DELETE.workspaceId), + id: z.string().trim().describe(ENVIRONMENTS.DELETE.id) }), 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 09de72233c..aece95a5d4 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -1,9 +1,18 @@ +import ms from "ms"; import { z } from "zod"; -import { OrgMembershipsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { + OrgMembershipsSchema, + ProjectMembershipsSchema, + ProjectUserMembershipRolesSchema, + UserEncryptionKeysSchema, + UsersSchema +} from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PROJECTS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ @@ -18,20 +27,35 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(PROJECTS.GET_USER_MEMBERSHIPS.workspaceId) }), response: { 200: z.object({ - memberships: ProjectMembershipsSchema.merge( - z.object({ - user: UsersSchema.pick({ - email: true, - firstName: true, - lastName: true, - id: true - }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })) - }) - ) + memberships: ProjectMembershipsSchema.omit({ role: true }) + .merge( + z.object({ + user: UsersSchema.pick({ + email: true, + firstName: true, + lastName: true, + id: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ) + }) + ) .omit({ createdAt: true, updatedAt: true }) .array() }) @@ -111,43 +135,61 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } ], params: z.object({ - workspaceId: z.string().trim(), - membershipId: z.string().trim() + workspaceId: z.string().trim().describe(PROJECTS.UPDATE_USER_MEMBERSHIP.workspaceId), + membershipId: z.string().trim().describe(PROJECTS.UPDATE_USER_MEMBERSHIP.membershipId) }), body: z.object({ - role: z.string().trim() + roles: z + .array( + z.union([ + z.object({ + role: z.string(), + isTemporary: z.literal(false).default(false) + }), + z.object({ + role: z.string(), + isTemporary: z.literal(true), + temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + ]) + ) + .min(1) + .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least long lived role is required") + .describe(PROJECTS.UPDATE_USER_MEMBERSHIP.roles) }), response: { 200: z.object({ - membership: ProjectMembershipsSchema + roles: ProjectUserMembershipRolesSchema.array() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.updateProjectMembership({ + const roles = await server.services.projectMembership.updateProjectMembership({ actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, projectId: req.params.workspaceId, membershipId: req.params.membershipId, - role: req.body.role + roles: req.body.roles }); - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: req.params.workspaceId, - event: { - type: EventType.UPDATE_USER_WORKSPACE_ROLE, - metadata: { - userId: membership.userId, - newRole: req.body.role, - oldRole: membership.role, - email: "" - } - } - }); - return { membership }; + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: req.params.workspaceId, + // event: { + // type: EventType.UPDATE_USER_WORKSPACE_ROLE, + // metadata: { + // userId: membership.userId, + // newRole: req.body.role, + // oldRole: membership.role, + // email: "" + // } + // } + // }); + return { roles }; } }); diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index f0faaa0f46..3ffedf98d7 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -2,13 +2,12 @@ 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 { PROJECTS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -62,16 +61,32 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - users: ProjectMembershipsSchema.merge( - z.object({ - user: UsersSchema.pick({ - email: true, - firstName: true, - lastName: true, - id: true - }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })) - }) - ) + users: ProjectMembershipsSchema.omit({ role: true }) + .merge( + z.object({ + user: UsersSchema.pick({ + username: true, + email: true, + firstName: true, + lastName: true, + id: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ) + }) + ) .omit({ createdAt: true, updatedAt: true }) .array() }) @@ -111,7 +126,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { method: "GET", schema: { params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(PROJECTS.GET.workspaceId) }), response: { 200: z.object({ @@ -119,7 +134,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, @@ -163,7 +178,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { method: "DELETE", schema: { params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(PROJECTS.DELETE.workspaceId) }), response: { 200: z.object({ @@ -171,7 +186,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 +231,46 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + url: "/:workspaceId", + method: "PATCH", + schema: { + params: z.object({ + workspaceId: z.string().trim().describe(PROJECTS.UPDATE.workspaceId) + }), + body: z.object({ + name: z + .string() + .trim() + .max(64, { message: "Name must be 64 or fewer characters" }) + .optional() + .describe(PROJECTS.UPDATE.name), + autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) + }), + 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 +304,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/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index 8a80fb7281..dee075943e 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { SecretFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { FOLDERS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -19,12 +20,12 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => } ], body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - name: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash), + workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId), + environment: z.string().trim().describe(FOLDERS.CREATE.environment), + name: z.string().trim().describe(FOLDERS.CREATE.name), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.path), // backward compatiability with cli - directory: z.string().trim().default("/").transform(removeTrailingSlash) + directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.CREATE.directory) }), response: { 200: z.object({ @@ -73,15 +74,15 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => ], params: z.object({ // old way this was name - folderId: z.string() + folderId: z.string().describe(FOLDERS.UPDATE.folderId) }), body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - name: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash), + workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId), + environment: z.string().trim().describe(FOLDERS.UPDATE.environment), + name: z.string().trim().describe(FOLDERS.UPDATE.name), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path), // backward compatiability with cli - directory: z.string().trim().default("/").transform(removeTrailingSlash) + directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.directory) }), response: { 200: z.object({ @@ -119,8 +120,9 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => } }); + // TODO(daniel): Expose this route in api reference and write docs for it. server.route({ - url: "/:folderId", + url: "/:folderIdOrName", method: "DELETE", schema: { description: "Delete a folder", @@ -131,14 +133,14 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => } ], params: z.object({ - folderId: z.string() + folderIdOrName: z.string().describe(FOLDERS.DELETE.folderIdOrName) }), body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash), + workspaceId: z.string().trim().describe(FOLDERS.DELETE.workspaceId), + environment: z.string().trim().describe(FOLDERS.DELETE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.path), // keep this here as cli need directory - directory: z.string().trim().default("/").transform(removeTrailingSlash) + directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.DELETE.directory) }), response: { 200: z.object({ @@ -155,7 +157,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, ...req.body, projectId: req.body.workspaceId, - id: req.params.folderId, + idOrName: req.params.folderIdOrName, path }); await server.services.auditLog.createAuditLog({ @@ -187,11 +189,11 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => } ], querystring: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash), + workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId), + environment: z.string().trim().describe(FOLDERS.LIST.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.path), // backward compatiability with cli - directory: z.string().trim().default("/").transform(removeTrailingSlash) + directory: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.LIST.directory) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/secret-import-router.ts b/backend/src/server/routes/v1/secret-import-router.ts index 2ec2d5ce2e..823e7dbee0 100644 --- a/backend/src/server/routes/v1/secret-import-router.ts +++ b/backend/src/server/routes/v1/secret-import-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { SecretImportsSchema, SecretsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SECRET_IMPORTS } from "@app/lib/api-docs"; import { removeTrailingSlash } from "@app/lib/fn"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -19,12 +20,12 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => } ], body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash), + workspaceId: z.string().trim().describe(SECRET_IMPORTS.CREATE.workspaceId), + environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.path), import: z.object({ - environment: z.string().trim(), - path: z.string().trim().transform(removeTrailingSlash) + environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment), + path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path) }) }), response: { @@ -80,20 +81,21 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => } ], params: z.object({ - secretImportId: z.string().trim() + secretImportId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.secretImportId) }), body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash), + workspaceId: z.string().trim().describe(SECRET_IMPORTS.UPDATE.workspaceId), + environment: z.string().trim().describe(SECRET_IMPORTS.UPDATE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.UPDATE.path), import: z.object({ - environment: z.string().trim().optional(), + environment: z.string().trim().optional().describe(SECRET_IMPORTS.UPDATE.import.environment), path: z .string() .trim() .optional() - .transform((val) => (val ? removeTrailingSlash(val) : val)), - position: z.number().optional() + .transform((val) => (val ? removeTrailingSlash(val) : val)) + .describe(SECRET_IMPORTS.UPDATE.import.path), + position: z.number().optional().describe(SECRET_IMPORTS.UPDATE.import.position) }) }), response: { @@ -150,12 +152,12 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => } ], params: z.object({ - secretImportId: z.string().trim() + secretImportId: z.string().trim().describe(SECRET_IMPORTS.DELETE.secretImportId) }), body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash) + workspaceId: z.string().trim().describe(SECRET_IMPORTS.DELETE.workspaceId), + environment: z.string().trim().describe(SECRET_IMPORTS.DELETE.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.DELETE.path) }), response: { 200: z.object({ @@ -210,9 +212,9 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) => } ], querystring: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - path: z.string().trim().default("/").transform(removeTrailingSlash) + workspaceId: z.string().trim().describe(SECRET_IMPORTS.LIST.workspaceId), + environment: z.string().trim().describe(SECRET_IMPORTS.LIST.environment), + path: z.string().trim().default("/").transform(removeTrailingSlash).describe(SECRET_IMPORTS.LIST.path) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index bfcf2f6ae5..60bbec7db9 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -18,7 +18,6 @@ import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { fetchGithubEmails } from "@app/lib/requests/github"; import { AuthMethod } from "@app/services/auth/auth-type"; -import { getServerCfg } from "@app/services/super-admin/super-admin-service"; export const registerSsoRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); @@ -42,7 +41,6 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { async (req, _accessToken, _refreshToken, profile, cb) => { try { const email = profile?.emails?.[0]?.value; - const serverCfg = await getServerCfg(); if (!email) throw new BadRequestError({ message: "Email not found", @@ -54,8 +52,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { firstName: profile?.name?.givenName || "", lastName: profile?.name?.familyName || "", authMethod: AuthMethod.GOOGLE, - callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp) + callbackPort: req.query.state as string }); cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -84,14 +81,12 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { try { const ghEmails = await fetchGithubEmails(accessToken); const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0]; - const serverCfg = await getServerCfg(); const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ email, firstName: profile.displayName, lastName: "", authMethod: AuthMethod.GITHUB, - callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp) + callbackPort: req.query.state as string }); return cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -120,14 +115,12 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => { try { const email = profile.emails[0].value; - const serverCfg = await getServerCfg(); const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ email, firstName: profile.displayName, lastName: "", authMethod: AuthMethod.GITLAB, - callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp) + callbackPort: req.query.state as string }); return cb(null, { isUserCompleted, providerAuthToken }); diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 1832e89621..97477b0332 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; +import { ORGANIZATIONS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,7 +19,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - orgId: z.string().trim() + orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index fdd810b4b6..67dccb5e3c 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -1,13 +1,16 @@ +import ms from "ms"; import { z } from "zod"; import { IdentitiesSchema, IdentityProjectMembershipsSchema, ProjectMembershipRole, - ProjectRolesSchema + ProjectUserMembershipRolesSchema } from "@app/db/schemas"; +import { PROJECTS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => { server.route({ @@ -53,28 +56,45 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) } ], params: z.object({ - projectId: z.string().trim(), - identityId: z.string().trim() + projectId: z.string().trim().describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.projectId), + identityId: z.string().trim().describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.identityId) }), body: z.object({ - role: z.string().trim().min(1).default(ProjectMembershipRole.NoAccess) + roles: z + .array( + z.union([ + z.object({ + role: z.string(), + isTemporary: z.literal(false).default(false) + }), + z.object({ + role: z.string(), + isTemporary: z.literal(true), + temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + ]) + ) + .min(1) + .describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.roles) }), response: { 200: z.object({ - identityMembership: IdentityProjectMembershipsSchema + roles: ProjectUserMembershipRolesSchema.array() }) } }, handler: async (req) => { - const identityMembership = await server.services.identityProject.updateProjectIdentity({ + const roles = await server.services.identityProject.updateProjectIdentity({ actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, identityId: req.params.identityId, projectId: req.params.projectId, - role: req.body.role + roles: req.body.roles }); - return { identityMembership }; + return { roles }; } }); @@ -90,8 +110,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) } ], params: z.object({ - projectId: z.string().trim(), - identityId: z.string().trim() + projectId: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.projectId), + identityId: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.identityId) }), response: { 200: z.object({ @@ -123,22 +143,33 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) } ], params: z.object({ - projectId: z.string().trim() + projectId: z.string().trim().describe(PROJECTS.LIST_IDENTITY_MEMBERSHIPS.projectId) }), response: { 200: z.object({ - identityMemberships: IdentityProjectMembershipsSchema.merge( - z.object({ - customRole: ProjectRolesSchema.pick({ - id: true, - name: true, - slug: true, - permissions: true, - description: true - }).optional(), + identityMemberships: z + .object({ + id: z.string(), + identityId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) }) - ).array() + .array() }) } }, diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts index 1f423c084f..deebbc9817 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/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 01ef7973ac..7d5ba3da7a 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { OrganizationsSchema, OrgMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { ORGANIZATIONS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -17,13 +18,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - organizationId: z.string().trim() + organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_USER_MEMBERSHIPS.organizationId) }), response: { 200: z.object({ users: OrgMembershipsSchema.merge( z.object({ user: UsersSchema.pick({ + username: true, email: true, firstName: true, lastName: true, @@ -61,7 +63,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - organizationId: z.string().trim() + organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId) }), response: { 200: z.object({ @@ -105,9 +107,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { apiKeyAuth: [] } ], - params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }), + params: z.object({ + organizationId: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.organizationId), + membershipId: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.membershipId) + }), body: z.object({ - role: z.string().trim() + role: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role) }), response: { 200: z.object({ @@ -141,7 +146,10 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { apiKeyAuth: [] } ], - params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }), + params: z.object({ + organizationId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.organizationId), + membershipId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.membershipId) + }), response: { 200: z.object({ membership: OrgMembershipsSchema @@ -179,11 +187,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { handler: async (req) => { if (req.auth.actor !== ActorType.USER) return; - const organization = await server.services.org.createOrganization( - req.permission.id, - req.auth.user.email, - req.body.name - ); + const organization = await server.services.org.createOrganization({ + userId: req.permission.id, + userEmail: req.auth.user.email, + orgName: req.body.name + }); + return { organization }; } }); 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 0000000000..6f81f83920 --- /dev/null +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -0,0 +1,100 @@ +import { z } from "zod"; + +import { ProjectMembershipsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { PROJECTS } from "@app/lib/api-docs"; +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(PROJECTS.INVITE_MEMBER.projectId) + }), + body: z.object({ + emails: z.string().email().array().default([]).describe(PROJECTS.INVITE_MEMBER.emails), + usernames: z.string().array().default([]).describe(PROJECTS.INVITE_MEMBER.usernames) + }), + 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, + usernames: req.body.usernames + }); + + 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(PROJECTS.REMOVE_MEMBER.projectId) + }), + + body: z.object({ + emails: z.string().email().array().default([]).describe(PROJECTS.REMOVE_MEMBER.emails), + usernames: z.string().array().default([]).describe(PROJECTS.REMOVE_MEMBER.usernames) + }), + 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, + usernames: req.body.usernames + }); + + 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 d38efdbb96..208edba7b9 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -1,11 +1,24 @@ +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 { PROJECTS } from "@app/lib/api-docs"; +import { authRateLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +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", @@ -17,7 +30,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(PROJECTS.GET_KEY.workspaceId) }), response: { 200: ProjectKeysSchema.merge( @@ -34,8 +47,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 +65,108 @@ 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().describe(PROJECTS.CREATE.projectName), + slug: z + .string() + .min(5) + .max(36) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .optional() + .describe(PROJECTS.CREATE.slug), + organizationId: z.string().trim().describe(PROJECTS.CREATE.organizationId) + }), + 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 + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.ProjectCreated, + distinctId: getTelemetryDistinctId(req), + properties: { + orgId: req.body.organizationId, + name: project.name, + ...req.auditLogInfo + } + }); + + return { project }; + } + }); }; diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 8fc114e766..97bc3d864b 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/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 0cda8e5fff..240aa21b1e 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -12,7 +12,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - email: z.string().email().trim(), + email: z.string().trim(), providerAuthToken: z.string().trim().optional(), clientPublicKey: z.string().trim() }), @@ -42,7 +42,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - email: z.string().email().trim(), + email: z.string().trim(), providerAuthToken: z.string().trim().optional(), clientProof: z.string().trim() }), diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 5f47ae3c5f..65219d0ab9 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1,4 +1,3 @@ -import { FastifyRequest } from "fastify"; import picomatch from "picomatch"; import { z } from "zod"; @@ -11,8 +10,10 @@ import { } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; +import { RAW_SECRETS } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -20,19 +21,6 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { secretRawSchema } from "../sanitizedSchemas"; -const getDistinctId = (req: FastifyRequest) => { - if (req.auth.actor === ActorType.USER) { - return req.auth.user.email; - } - if (req.auth.actor === ActorType.IDENTITY) { - return `identity-${req.auth.identityId}`; - } - if (req.auth.actor === ActorType.SERVICE) { - return `service-token-${req.auth.serviceToken.id}`; - } - return "unknown-auth-data"; -}; - export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ url: "/raw", @@ -46,13 +34,14 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ - workspaceId: z.string().trim().optional(), - environment: z.string().trim().optional(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), + environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), include_imports: z .enum(["true", "false"]) .default("false") .transform((value) => value === "true") + .describe(RAW_SECRETS.LIST.includeImports) }), response: { 200: z.object({ @@ -108,9 +97,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretPulled, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: secrets.length, workspaceId, @@ -136,18 +125,19 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim() + secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) }), querystring: z.object({ - workspaceId: z.string().trim().optional(), - environment: z.string().trim().optional(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - version: z.coerce.number().optional(), - type: z.nativeEnum(SecretType).default(SecretType.Shared), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId), + environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), + version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), include_imports: z .enum(["true", "false"]) .default("false") .transform((value) => value === "true") + .describe(RAW_SECRETS.GET.includeImports) }), response: { 200: z.object({ @@ -198,9 +188,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretPulled, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId, @@ -226,16 +216,24 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim() + secretName: z.string().trim().describe(RAW_SECRETS.CREATE.secretName) }), body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - secretValue: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())), - secretComment: z.string().trim().optional().default(""), - skipMultilineEncoding: z.boolean().optional(), - type: z.nativeEnum(SecretType).default(SecretType.Shared) + workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId), + environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.CREATE.secretPath), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.CREATE.secretValue), + secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type) }), response: { 200: z.object({ @@ -274,9 +272,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretCreated, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId: req.body.workspaceId, @@ -303,15 +301,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim() + secretName: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName) }), body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - secretValue: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - skipMultilineEncoding: z.boolean().optional(), - type: z.nativeEnum(SecretType).default(SecretType.Shared) + workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId), + environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.UPDATE.secretValue), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.UPDATE.secretPath), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type) }), response: { 200: z.object({ @@ -349,9 +355,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretUpdated, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId: req.body.workspaceId, @@ -377,13 +383,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim() + secretName: z.string().trim().describe(RAW_SECRETS.DELETE.secretName) }), body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - type: z.nativeEnum(SecretType).default(SecretType.Shared) + workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId), + environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.DELETE.secretPath), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.DELETE.type) }), response: { 200: z.object({ @@ -419,9 +430,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretDeleted, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId: req.body.workspaceId, @@ -525,9 +536,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { (req.headers["user-agent"] !== "k8-operator" || shouldRecordK8Event); const approximateNumberTotalSecrets = secrets.length * 20; if (shouldCapture) { - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretPulled, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: shouldRecordK8Event ? approximateNumberTotalSecrets : secrets.length, workspaceId: req.query.workspaceId, @@ -602,9 +613,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretPulled, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId: req.query.workspaceId, @@ -765,9 +776,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretCreated, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId: req.body.workspaceId, @@ -947,9 +958,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretUpdated, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId: req.body.workspaceId, @@ -1065,9 +1076,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretDeleted, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, workspaceId: req.body.workspaceId, @@ -1092,7 +1103,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 +1149,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { projectId, policy, data: { - [CommitType.Create]: inputSecrets.filter(({ type }) => type === "shared") + [CommitType.Create]: inputSecrets } }); @@ -1186,9 +1196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretCreated, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: secrets.length, workspaceId: req.body.workspaceId, @@ -1306,9 +1316,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretUpdated, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: secrets.length, workspaceId: req.body.workspaceId, @@ -1414,9 +1424,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); - server.services.telemetry.sendPostHogEvents({ + await server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.SecretDeleted, - distinctId: getDistinctId(req), + distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: secrets.length, workspaceId: req.body.workspaceId, diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 15264b9268..17787be843 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -2,7 +2,9 @@ import { z } from "zod"; import { UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; import { authRateLimit } from "@app/server/config/rateLimiter"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerSignupRouter = async (server: FastifyZodProvider) => { @@ -23,8 +25,26 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - await server.services.signup.beginEmailSignupProcess(req.body.email); - return { message: `Sent an email verification code to ${req.body.email}` }; + const { email } = req.body; + + const serverCfg = await getServerCfg(); + if (!serverCfg.allowSignUp) { + throw new BadRequestError({ + message: "Sign up is disabled" + }); + } + + if (serverCfg?.allowedSignUpDomain) { + const domain = email.split("@")[1]; + const allowedDomains = serverCfg.allowedSignUpDomain.split(",").map((e) => e.trim()); + if (!allowedDomains.includes(domain)) { + throw new BadRequestError({ + message: `Email with a domain (@${domain}) is not supported` + }); + } + } + await server.services.signup.beginEmailSignupProcess(email); + return { message: `Sent an email verification code to ${email}` }; } }); @@ -48,6 +68,13 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { + const serverCfg = await getServerCfg(); + if (!serverCfg.allowSignUp) { + throw new BadRequestError({ + message: "Sign up is disabled" + }); + } + const { token, user } = await server.services.signup.verifyEmailSignup(req.body.email, req.body.code); return { message: "Successfuly verified email", token, user }; } @@ -61,7 +88,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - email: z.string().email().trim(), + email: z.string().trim(), firstName: z.string().trim(), lastName: z.string().trim().optional(), protectedKey: z.string().trim(), @@ -90,6 +117,13 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { if (!userAgent) throw new Error("user agent header is required"); const appCfg = getConfig(); + const serverCfg = await getServerCfg(); + if (!serverCfg.allowSignUp) { + throw new BadRequestError({ + message: "Sign up is disabled" + }); + } + const { user, accessToken, refreshToken } = await server.services.signup.completeEmailAccountSignup({ ...req.body, ip: req.realIp, @@ -97,13 +131,16 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { authorization: req.headers.authorization as string }); - void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); + if (user.email) { + void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); + } void server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.UserSignedUp, - distinctId: user.email, + distinctId: user.username ?? "", properties: { - email: user.email, + username: user.username, + email: user.email ?? "", attributionSource: req.body.attributionSource } }); @@ -156,16 +193,20 @@ 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 || ""); + if (user.email) { + void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || ""); + } void server.services.telemetry.sendPostHogEvents({ event: PostHogEventTypes.UserSignedUp, - distinctId: user.email, + distinctId: user.username ?? "", properties: { - email: user.email, + username: user.username, + email: user.email ?? "", attributionSource: "Team Invite" } }); diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index b46803b06b..0b78ab438a 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -5,13 +5,14 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; -export const validateProviderAuthToken = (providerToken: string, email: string) => { +export const validateProviderAuthToken = (providerToken: string, username?: string) => { if (!providerToken) throw new UnauthorizedError(); const appCfg = getConfig(); const decodedToken = jwt.verify(providerToken, appCfg.AUTH_SECRET) as AuthModeProviderJwtTokenPayload; if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new UnauthorizedError(); - if (decodedToken.email !== email) throw new Error("Invalid auth credentials"); + + if (decodedToken.username !== username) throw new Error("Invalid auth credentials"); if (decodedToken.organizationId) { return { orgId: decodedToken.organizationId }; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 6e4d60bbab..786e69d735 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -4,6 +4,7 @@ import { TUsers, UserDeviceSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; @@ -38,17 +39,19 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: if (!isDeviceSeen) { const newDeviceList = devices.concat([{ ip, userAgent }]); await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }); - await smtpService.sendMail({ - template: SmtpTemplates.NewDeviceJoin, - subjectLine: "Successful login from new device", - recipients: [user.email], - substitutions: { - email: user.email, - timestamp: new Date().toString(), - ip, - userAgent - } - }); + if (user.email) { + await smtpService.sendMail({ + template: SmtpTemplates.NewDeviceJoin, + subjectLine: "Successful login from new device", + recipients: [user.email], + substitutions: { + email: user.email, + timestamp: new Date().toString(), + ip, + userAgent + } + }); + } } }; @@ -130,7 +133,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: providerAuthToken, clientPublicKey }: TLoginGenServerPublicKeyDTO) => { - const userEnc = await userDAL.findUserEncKeyByEmail(email); + const userEnc = await userDAL.findUserEncKeyByUsername({ + username: email + }); if (!userEnc || (userEnc && !userEnc.isAccepted)) { throw new Error("Failed to find user"); } @@ -157,7 +162,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: ip, userAgent }: TLoginClientProofDTO) => { - const userEnc = await userDAL.findUserEncKeyByEmail(email); + const userEnc = await userDAL.findUserEncKeyByUsername({ + username: email + }); if (!userEnc) throw new Error("Failed to find user"); const cfg = getConfig(); @@ -186,7 +193,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: clientPublicKey: null }); // send multi factor auth token if they it enabled - if (userEnc.isMfaEnabled) { + if (userEnc.isMfaEnabled && userEnc.email) { const mfaToken = jwt.sign( { authTokenType: AuthTokenType.MFA_TOKEN, @@ -226,7 +233,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: */ const resendMfaToken = async (userId: string) => { const user = await userDAL.findById(userId); - if (!user) return; + if (!user || !user.email) return; await sendUserMfaCode({ userId: user.id, email: user.email @@ -261,21 +268,34 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: /* * OAuth2 login for google,github, and other oauth2 provider * */ - const oauth2Login = async ({ - email, - firstName, - lastName, - authMethod, - callbackPort, - isSignupAllowed - }: TOauthLoginDTO) => { - let user = await userDAL.findUserByEmail(email); + const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => { + let user = await userDAL.findUserByUsername(email); + const serverCfg = await getServerCfg(); + const appCfg = getConfig(); - const isOauthSignUpDisabled = !isSignupAllowed && !user; - if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" }); if (!user) { - user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] }); + // Create a new user based on oAuth + if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" }); + + if (serverCfg?.allowedSignUpDomain) { + const domain = email.split("@")[1]; + const allowedDomains = serverCfg.allowedSignUpDomain.split(",").map((e) => e.trim()); + if (!allowedDomains.includes(domain)) + throw new BadRequestError({ + message: `Email with a domain (@${domain}) is not supported`, + name: "Oauth 2 login" + }); + } + + user = await userDAL.create({ + username: email, + email, + firstName, + lastName, + authMethods: [authMethod], + isGhost: false + }); } const isLinkingRequired = !user?.authMethods?.includes(authMethod); const isUserCompleted = user.isAccepted; @@ -283,7 +303,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, - email: user.email, + username: user.username, firstName: user.firstName, lastName: user.lastName, authMethod, diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index 67f640bc9e..86af5a5f9e 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -28,5 +28,4 @@ export type TOauthLoginDTO = { lastName?: string; authMethod: AuthMethod; callbackPort?: string; - isSignupAllowed?: boolean; }; diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index ff07d422f3..42b8c2c39a 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -99,7 +99,7 @@ export const authPaswordServiceFactory = ({ * Email password reset flow via email. Step 1 send email */ const sendPasswordResetEmail = async (email: string) => { - const user = await userDAL.findUserByEmail(email); + const user = await userDAL.findUserByUsername(email); // ignore as user is not found to avoid an outside entity to identify infisical registered accounts if (!user || (user && !user.isAccepted)) return; @@ -126,7 +126,7 @@ export const authPaswordServiceFactory = ({ * */ const verifyPasswordResetEmail = async (email: string, code: string) => { const cfg = getConfig(); - const user = await userDAL.findUserByEmail(email); + const user = await userDAL.findUserByUsername(email); // ignore as user is not found to avoid an outside entity to identify infisical registered accounts if (!user || (user && !user.isAccepted)) { throw new Error("Failed email verification for pass reset"); diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 752d5d5639..39bfec8b12 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -44,13 +44,13 @@ export const authSignupServiceFactory = ({ throw new Error("Provided a disposable email"); } - let user = await userDAL.findUserByEmail(email); + let user = await userDAL.findUserByUsername(email); if (user && user.isAccepted) { // TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues 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], username: email, email, isGhost: false }); } if (!user) throw new Error("Failed to create user"); @@ -70,7 +70,7 @@ export const authSignupServiceFactory = ({ }; const verifyEmailSignup = async (email: string, code: string) => { - const user = await userDAL.findUserByEmail(email); + const user = await userDAL.findUserByUsername(email); if (!user || (user && user.isAccepted)) { // TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues throw new Error("Failed to send verification code for complete account"); @@ -115,14 +115,14 @@ export const authSignupServiceFactory = ({ userAgent, authorization }: TCompleteAccountSignupDTO) => { - const user = await userDAL.findUserByEmail(email); + const user = await userDAL.findOne({ username: email }); if (!user || (user && user.isAccepted)) { throw new Error("Failed to complete account for complete user"); } let organizationId; if (providerAuthToken) { - const { orgId } = validateProviderAuthToken(providerAuthToken, user.email); + const { orgId } = validateProviderAuthToken(providerAuthToken, user.username); organizationId = orgId; } else { validateSignUpAuthorization(authorization, user.id); @@ -150,7 +150,11 @@ export const authSignupServiceFactory = ({ }); if (!organizationId) { - await orgService.createOrganization(user.id, user.email, organizationName); + await orgService.createOrganization({ + userId: user.id, + userEmail: user.email ?? user.username, + orgName: organizationName + }); } const updatedMembersips = await orgDAL.updateMembership( @@ -212,13 +216,16 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, encryptedPrivateKeyIV, - encryptedPrivateKeyTag + encryptedPrivateKeyTag, + authorization }: TCompleteAccountInviteDTO) => { - const user = await userDAL.findUserByEmail(email); + const user = await userDAL.findUserByUsername(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 69b779e8be..a37a1cd96d 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 bea0dbe10c..57c86158f4 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -5,7 +5,8 @@ export enum AuthMethod { GITLAB = "gitlab", OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", - JUMPCLOUD_SAML = "jumpcloud-saml" + JUMPCLOUD_SAML = "jumpcloud-saml", + LDAP = "ldap" } export enum AuthTokenType { @@ -17,21 +18,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 = { @@ -58,7 +62,7 @@ export type AuthModeRefreshJwtTokenPayload = { export type AuthModeProviderJwtTokenPayload = { authTokenType: AuthTokenType.PROVIDER_TOKEN; - email: string; + username: string; organizationId?: string; }; 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 cdc8effe2d..32774ccbb0 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-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index dbb864387a..dd3ba04f48 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -3,7 +3,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, sqlNestRelationships } from "@app/lib/knex"; export type TIdentityProjectDALFactory = ReturnType; @@ -15,52 +15,81 @@ export const identityProjectDALFactory = (db: TDbClient) => { const docs = await (tx || db)(TableName.IdentityProjectMembership) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) + .join( + TableName.IdentityProjectMembershipRole, + `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) .leftJoin( TableName.ProjectRoles, - `${TableName.IdentityProjectMembership}.roleId`, + `${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) - .select(selectAllTableCols(TableName.IdentityProjectMembership)) - // cr stands for custom role - .select(db.ref("id").as("crId").withSchema(TableName.ProjectRoles)) - .select(db.ref("name").as("crName").withSchema(TableName.ProjectRoles)) - .select(db.ref("slug").as("crSlug").withSchema(TableName.ProjectRoles)) - .select(db.ref("description").as("crDescription").withSchema(TableName.ProjectRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.ProjectRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.ProjectRoles)) - .select(db.ref("id").as("identityId").withSchema(TableName.Identity)) - .select(db.ref("name").as("identityName").withSchema(TableName.Identity)) - .select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)); - return docs.map( - ({ - crId, - crDescription, - crSlug, - crPermission, - crName, - identityId, - identityName, - identityAuthMethod, - ...el - }) => ({ - ...el, + .select( + db.ref("id").withSchema(TableName.IdentityProjectMembership), + db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), + db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), + db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity), + db.ref("id").as("identityId").withSchema(TableName.Identity), + db.ref("name").as("identityName").withSchema(TableName.Identity), + db.ref("id").withSchema(TableName.IdentityProjectMembership), + db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole) + ); + + const members = sqlNestRelationships({ + data: docs, + parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt }) => ({ + id, identityId, + createdAt, + updatedAt, identity: { id: identityId, name: identityName, authMethod: identityAuthMethod - }, - customRole: el.roleId - ? { - id: crId, - name: crName, - slug: crSlug, - permissions: crPermission, - description: crDescription - } - : undefined - }) - ); + } + }), + key: "id", + childrenMapper: [ + { + label: "roles" as const, + key: "membershipRoleId", + mapper: ({ + role, + customRoleId, + customRoleName, + customRoleSlug, + membershipRoleId, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) => ({ + id: membershipRoleId, + role, + customRoleId, + customRoleName, + customRoleSlug, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) + } + ] + }); + return members; } catch (error) { throw new DatabaseError({ error, name: "FindByProjectId" }); } diff --git a/backend/src/services/identity-project/identity-project-membership-role-dal.ts b/backend/src/services/identity-project/identity-project-membership-role-dal.ts new file mode 100644 index 0000000000..3f6c6b589b --- /dev/null +++ b/backend/src/services/identity-project/identity-project-membership-role-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 TIdentityProjectMembershipRoleDALFactory = ReturnType; + +export const identityProjectMembershipRoleDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.IdentityProjectMembershipRole); + return orm; +}; diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index f9d21034f3..b6f6e4343d 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -1,15 +1,20 @@ import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; -import { ProjectMembershipRole, TProjectRoles } from "@app/db/schemas"; +import { ProjectMembershipRole } from "@app/db/schemas"; 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 { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; import { ActorType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TProjectDALFactory } from "../project/project-dal"; +import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; +import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TIdentityProjectDALFactory } from "./identity-project-dal"; +import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-membership-role-dal"; import { TCreateProjectIdentityDTO, TDeleteProjectIdentityDTO, @@ -19,7 +24,12 @@ import { type TIdentityProjectServiceFactoryDep = { identityProjectDAL: TIdentityProjectDALFactory; + identityProjectMembershipRoleDAL: Pick< + TIdentityProjectMembershipRoleDALFactory, + "create" | "transaction" | "insertMany" | "delete" + >; projectDAL: Pick; + projectRoleDAL: Pick; identityOrgMembershipDAL: Pick; permissionService: Pick; }; @@ -30,7 +40,9 @@ export const identityProjectServiceFactory = ({ identityProjectDAL, permissionService, identityOrgMembershipDAL, - projectDAL + identityProjectMembershipRoleDAL, + projectDAL, + projectRoleDAL }: TIdentityProjectServiceFactoryDep) => { const createProjectIdentity = async ({ identityId, @@ -70,11 +82,26 @@ export const identityProjectServiceFactory = ({ }); const isCustomRole = Boolean(customRole); - const projectIdentity = await identityProjectDAL.create({ - identityId, - projectId: project.id, - role: isCustomRole ? ProjectMembershipRole.Custom : role, - roleId: customRole?.id + const projectIdentity = await identityProjectDAL.transaction(async (tx) => { + const identityProjectMembership = await identityProjectDAL.create( + { + identityId, + projectId: project.id, + role: isCustomRole ? ProjectMembershipRole.Custom : role, + roleId: customRole?.id + }, + tx + ); + + await identityProjectMembershipRoleDAL.create( + { + projectMembershipId: identityProjectMembership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : role, + customRoleId: customRole?.id + }, + tx + ); + return identityProjectMembership; }); return projectIdentity; }; @@ -82,7 +109,7 @@ export const identityProjectServiceFactory = ({ const updateProjectIdentity = async ({ projectId, identityId, - role, + roles, actor, actorId, actorOrgId @@ -106,28 +133,51 @@ export const identityProjectServiceFactory = ({ if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); - let customRole: TProjectRoles | undefined; - if (role) { - const { permission: rolePermission, role: customOrgRole } = await permissionService.getProjectPermissionByRole( - role, - projectIdentity.projectId - ); - - const isCustomRole = Boolean(customOrgRole); - const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasRequiredNewRolePermission) - throw new BadRequestError({ message: "Failed to create a more privileged identity" }); - if (isCustomRole) customRole = customOrgRole; - } - - const [updatedProjectIdentity] = await identityProjectDAL.update( - { projectId, identityId: projectIdentity.identityId }, - { - role: customRole ? ProjectMembershipRole.Custom : role, - roleId: customRole ? customRole.id : null - } + // validate custom roles input + const customInputRoles = roles.filter( + ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) ); - return updatedProjectIdentity; + const hasCustomRole = Boolean(customInputRoles.length); + const customRoles = hasCustomRole + ? await projectRoleDAL.find({ + projectId, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" }); + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const santiziedProjectMembershipRoles = roles.map((inputRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); + if (!inputRole.isTemporary) { + return { + projectMembershipId: projectIdentity.id, + role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, + customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null + }; + } + + // check cron or relative here later for now its just relative + const relativeTimeInMs = ms(inputRole.temporaryRange); + return { + projectMembershipId: projectIdentity.id, + role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, + customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, + isTemporary: true, + temporaryMode: ProjectUserMembershipTemporaryMode.Relative, + temporaryRange: inputRole.temporaryRange, + temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) + }; + }); + + const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => { + await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx); + return identityProjectMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx); + }); + + return updatedRoles; }; const deleteProjectIdentity = async ({ diff --git a/backend/src/services/identity-project/identity-project-types.ts b/backend/src/services/identity-project/identity-project-types.ts index 71e048c199..73e8ec2469 100644 --- a/backend/src/services/identity-project/identity-project-types.ts +++ b/backend/src/services/identity-project/identity-project-types.ts @@ -1,12 +1,26 @@ import { TProjectPermission } from "@app/lib/types"; +import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; + export type TCreateProjectIdentityDTO = { identityId: string; role: string; } & TProjectPermission; export type TUpdateProjectIdentityDTO = { - role: string; + roles: ( + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: ProjectUserMembershipTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } + )[]; identityId: string; } & TProjectPermission; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 60e748ff76..d375a8fa54 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -54,6 +54,8 @@ export const identityUaServiceFactory = ({ const identityUa = await identityUaDAL.findOne({ clientId }); if (!identityUa) throw new UnauthorizedError(); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId }); + checkIPAgainstBlocklist({ ipAddress: ip, trustedIps: identityUa.clientSecretTrustedIps as TIp[] @@ -69,9 +71,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,11 +126,14 @@ export const identityUaServiceFactory = ({ } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, { - expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL + expiresIn: + Number(identityAccessToken.accessTokenMaxTTL) === 0 + ? undefined + : Number(identityAccessToken.accessTokenMaxTTL) } ); - return { accessToken, identityUa, validClientSecretInfo, identityAccessToken }; + return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg }; }; const attachUa = async ({ diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 17b1b63ad7..17c0051c89 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -109,7 +109,7 @@ const getAppsGCPSecretManager = async ({ accessToken }: { accessToken: string }) */ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { const res = ( - await request.get<{ name: string }[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, { + await request.get<{ name: string; id: string }[]>(`${IntegrationUrls.HEROKU_API_URL}/apps`, { headers: { Accept: "application/vnd.heroku+json; version=3", Authorization: `Bearer ${accessToken}` @@ -118,7 +118,8 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { ).data; const apps = res.map((a) => ({ - name: a.name + name: a.name, + appId: a.id })); return apps; @@ -259,20 +260,44 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { * Return list of services for Render integration */ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { - const res = ( - await request.get<{ service: { name: string; id: string } }[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json", - "Accept-Encoding": "application/json" - } - }) - ).data; + const apps: Array<{ name: string; appId: string }> = []; + let hasMorePages = true; + const perPage = 100; + let cursor; - const apps = res.map((a) => ({ - name: a.service.name, - appId: a.service.id - })); + interface RenderService { + cursor: string; + service: { name: string; id: string }; + } + + while (hasMorePages) { + const res: RenderService[] = ( + await request.get(`${IntegrationUrls.RENDER_API_URL}/v1/services`, { + params: new URLSearchParams({ + ...(cursor ? { cursor: String(cursor) } : {}), + limit: String(perPage) + }), + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Accept-Encoding": "application/json" + } + }) + ).data; + + res.forEach((a) => { + apps.push({ + name: a.service.name, + appId: a.service.id + }); + }); + + if (res.length < perPage) { + hasMorePages = false; + } else { + cursor = res[res.length - 1].cursor; + } + } return apps; }; diff --git a/backend/src/services/integration-auth/integration-auth-dal.ts b/backend/src/services/integration-auth/integration-auth-dal.ts index f2d9c9ea21..d32cd1579e 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/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 4b8d0ee809..8f6b766d19 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -21,11 +21,13 @@ import { TDeleteIntegrationAuthsDTO, TGetIntegrationAuthDTO, TGetIntegrationAuthTeamCityBuildConfigDTO, + THerokuPipelineCoupling, TIntegrationAuthAppsDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthGithubEnvsDTO, TIntegrationAuthGithubOrgsDTO, + TIntegrationAuthHerokuPipelinesDTO, TIntegrationAuthNorthflankSecretGroupDTO, TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryOrgsDTO, @@ -644,6 +646,38 @@ export const integrationAuthServiceFactory = ({ return []; }; + const getHerokuPipelines = async ({ id, actor, actorId, actorOrgId }: TIntegrationAuthHerokuPipelinesDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const botKey = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); + + const { data } = await request.get( + `${IntegrationUrls.HEROKU_API_URL}/pipeline-couplings`, + { + headers: { + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + return data.map(({ app: { id: appId }, stage, pipeline: { id: pipelineId, name } }) => ({ + app: { appId }, + stage, + pipeline: { pipelineId, name } + })); + }; + const getRailwayEnvironments = async ({ id, actor, actorId, actorOrgId, appId }: TIntegrationAuthRailwayEnvDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -717,33 +751,21 @@ export const integrationAuthServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); - if (appId) { + + if (appId && appId !== "") { const query = ` - query project($id: String!) { - project(id: $id) { - createdAt - deletedAt - id - description - expiredAt - isPublic - isTempProject - isUpdatable - name - prDeploys - teamId - updatedAt - upstreamUrl - services { - edges { - node { - id - name - } - } - } + query project($id: String!) { + project(id: $id) { + services { + edges { + node { + id + name + } + } + } + } } - } `; const variables = { @@ -779,6 +801,7 @@ export const integrationAuthServiceFactory = ({ ); return edges.map(({ node: { name, id: serviceId } }) => ({ name, serviceId })); } + return []; }; @@ -983,6 +1006,7 @@ export const integrationAuthServiceFactory = ({ getQoveryApps, getQoveryEnvs, getQoveryJobs, + getHerokuPipelines, getQoveryOrgs, getQoveryProjects, getQoveryContainers, diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 0c411b5738..0c8671fbfe 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -72,6 +72,10 @@ export type TIntegrationAuthQoveryScopesDTO = { environmentId: string; } & Omit; +export type TIntegrationAuthHerokuPipelinesDTO = { + id: string; +} & Omit; + export type TIntegrationAuthRailwayEnvDTO = { id: string; appId: string; @@ -139,6 +143,12 @@ export type TNorthflankSecretGroup = { projectId: string; }; +export type THerokuPipelineCoupling = { + app: { id: string }; + stage: string; + pipeline: { id: string; name: string }; +}; + export type TTeamCityBuildConfig = { id: string; name: string; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index d3cabbcb56..e49cd38627 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -37,6 +37,12 @@ export enum IntegrationType { OAUTH2 = "oauth2" } +export enum IntegrationInitialSyncBehavior { + OVERWRITE_TARGET = "overwrite-target", + PREFER_TARGET = "prefer-target", + PREFER_SOURCE = "prefer-source" +} + export enum IntegrationUrls { // integration oauth endpoints GCP_TOKEN_URL = "https://oauth2.googleapis.com/token", diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 5d4bc72c2d..7ac5813ed3 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -20,11 +20,13 @@ import sodium from "libsodium-wrappers"; import isEqual from "lodash.isequal"; import { z } from "zod"; -import { TIntegrationAuths, TIntegrations } from "@app/db/schemas"; +import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; +import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; -import { Integrations, IntegrationUrls } from "./integration-list"; +import { TIntegrationDALFactory } from "../integration/integration-dal"; +import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list"; const getSecretKeyValuePair = (secrets: Record) => Object.keys(secrets).reduce>((prev, key) => { @@ -441,20 +443,23 @@ const syncSecretsAWSParameterStore = async ({ }) => { if (!accessId) return; - AWS.config.update({ + const config = new AWS.Config({ region: integration.region as string, - accessKeyId: accessId, - secretAccessKey: accessToken + credentials: { + accessKeyId: accessId, + secretAccessKey: accessToken + } }); const ssm = new AWS.SSM({ apiVersion: "2014-11-06", region: integration.region as string }); + ssm.config.update(config); const params = { Path: integration.path as string, - Recursive: true, + Recursive: false, WithDecryption: true }; @@ -514,12 +519,6 @@ const syncSecretsAWSParameterStore = async ({ } }) ); - - AWS.config.update({ - region: undefined, - accessKeyId: undefined, - secretAccessKey: undefined - }); }; /** @@ -541,12 +540,6 @@ const syncSecretsAWSSecretManager = async ({ try { if (!accessId) return; - AWS.config.update({ - region: integration.region as string, - accessKeyId: accessId, - secretAccessKey: accessToken - }); - secretsManager = new SecretsManagerClient({ region: integration.region as string, credentials: { @@ -575,12 +568,6 @@ const syncSecretsAWSSecretManager = async ({ }) ); } - - AWS.config.update({ - region: undefined, - accessKeyId: undefined, - secretAccessKey: undefined - }); } catch (err) { if (err instanceof ResourceNotFoundException && secretsManager) { await secretsManager.send( @@ -590,11 +577,6 @@ const syncSecretsAWSSecretManager = async ({ }) ); } - AWS.config.update({ - region: undefined, - accessKeyId: undefined, - secretAccessKey: undefined - }); } }; @@ -602,11 +584,25 @@ const syncSecretsAWSSecretManager = async ({ * Sync/push [secrets] to Heroku app named [integration.app] */ const syncSecretsHeroku = async ({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, integration, secrets, accessToken }: { - integration: TIntegrations; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + integrationDAL: Pick; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; secrets: Record; accessToken: string; }) => { @@ -620,12 +616,74 @@ const syncSecretsHeroku = async ({ }) ).data; + const secretsToAdd: { [key: string]: string } = {}; + const secretsToUpdate: { [key: string]: string } = {}; + + const metadata = z.record(z.any()).parse(integration.metadata); + Object.keys(herokuSecrets).forEach((key) => { - if (!(key in secrets)) { - secrets[key] = null; - } + if (!integration.lastUsed) { + // first time using integration + // -> apply initial sync behavior + switch (metadata.initialSyncBehavior) { + case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: { + if (!(key in secrets)) secrets[key] = null; + break; + } + case IntegrationInitialSyncBehavior.PREFER_TARGET: { + if (!(key in secrets)) { + secretsToAdd[key] = herokuSecrets[key]; + } else if (secrets[key]?.value !== herokuSecrets[key]) { + secretsToUpdate[key] = herokuSecrets[key]; + } + secrets[key] = { + value: herokuSecrets[key] + }; + break; + } + case IntegrationInitialSyncBehavior.PREFER_SOURCE: { + if (!(key in secrets)) { + secrets[key] = herokuSecrets[key]; + secretsToAdd[key] = herokuSecrets[key]; + } + break; + } + default: { + if (!(key in secrets)) secrets[key] = null; + break; + } + } + } else if (!(key in secrets)) secrets[key] = null; }); + if (Object.keys(secretsToAdd).length) { + await createManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, + secrets: Object.keys(secretsToAdd).map((key) => ({ + secretName: key, + secretValue: secretsToAdd[key], + type: SecretType.Shared, + secretComment: "" + })) + }); + } + + if (Object.keys(secretsToUpdate).length) { + await updateManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, + secrets: Object.keys(secretsToUpdate).map((key) => ({ + secretName: key, + secretValue: secretsToUpdate[key], + type: SecretType.Shared, + secretComment: "" + })) + }); + } + await request.patch( `${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`, getSecretKeyValuePair(secrets), @@ -637,6 +695,10 @@ const syncSecretsHeroku = async ({ } } ); + + await integrationDAL.updateById(integration.id, { + lastUsed: new Date() + }); }; /** @@ -1325,21 +1387,21 @@ const syncSecretsRailway = async ({ } `; - const input = { - projectId: integration.appId, - environmentId: integration.targetEnvironmentId, - ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), - replace: true, - variables: getSecretKeyValuePair(secrets) + const variables = { + input: { + projectId: integration.appId, + environmentId: integration.targetEnvironmentId, + ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), + replace: true, + variables: getSecretKeyValuePair(secrets) + } }; await request.post( IntegrationUrls.RAILWAY_API_URL, { query, - variables: { - input - } + variables }, { headers: { @@ -3051,8 +3113,14 @@ const syncSecretsHasuraCloud = async ({ /** * Sync/push [secrets] to [app] in integration named [integration] + * + * Do this in terms of DAL + * */ export const syncIntegrationSecrets = async ({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, integration, integrationAuth, secrets, @@ -3060,7 +3128,18 @@ export const syncIntegrationSecrets = async ({ accessToken, appendices }: { - integration: TIntegrations; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; + integrationDAL: Pick; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; integrationAuth: TIntegrationAuths; secrets: Record; accessId: string | null; @@ -3100,6 +3179,9 @@ export const syncIntegrationSecrets = async ({ break; case Integrations.HEROKU: await syncSecretsHeroku({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, integration, secrets, accessToken diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index e914aac421..3b1daa8277 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -57,7 +57,7 @@ export const orgDALFactory = (db: TDbClient) => { const findAllOrgMembers = async (orgId: string) => { try { const members = await db(TableName.OrgMembership) - .where({ orgId }) + .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( TableName.UserEncryptionKey, @@ -72,11 +72,49 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users), + db.ref("username").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 }); // MAKE SURE USER IS NOT A GHOST USER + + return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({ + ...data, + user: { email, username, firstName, lastName, id: userId, publicKey } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find all org members" }); + } + }; + + const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => { + try { + const members = await db(TableName.OrgMembership) + .where(`${TableName.OrgMembership}.orgId`, 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("username").withSchema(TableName.Users), + 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("username", usernames); return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({ ...data, user: { email, firstName, lastName, id: userId, publicKey } @@ -86,6 +124,45 @@ export const orgDALFactory = (db: TDbClient) => { } }; + 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 +242,17 @@ 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("username").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users), + db.ref("scimEnabled").withSchema(TableName.Organization) + ) + .where({ isGhost: false }); + if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { @@ -184,6 +271,9 @@ export const orgDALFactory = (db: TDbClient) => { findAllOrgMembers, findOrgById, findAllOrgsByUserId, + ghostUserExists, + findOrgMembersByUsername, + 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 0000000000..ec6d4cb2d0 --- /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-role-service.ts b/backend/src/services/org/org-role-service.ts index b3d8121c3e..fb8a574405 100644 --- a/backend/src/services/org/org-role-service.ts +++ b/backend/src/services/org/org-role-service.ts @@ -58,7 +58,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol { id: roleId, orgId }, { ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined } ); - if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); + if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); return updatedRole; }; @@ -66,7 +66,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId }); - if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); + if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); return deletedRole; }; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 7fafca4387..db6d9654d7 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"; @@ -19,6 +22,8 @@ import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TProjectDALFactory } from "../project/project-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; @@ -28,6 +33,7 @@ import { TOrgRoleDALFactory } from "./org-role-dal"; import { TDeleteOrgMembershipDTO, TFindAllWorkspacesDTO, + TFindOrgMembersByEmailDTO, TInviteUserToOrgDTO, TUpdateOrgDTO, TUpdateOrgMembershipDTO, @@ -40,6 +46,8 @@ type TOrgServiceFactoryDep = { orgRoleDAL: TOrgRoleDALFactory; userDAL: TUserDALFactory; projectDAL: TProjectDALFactory; + projectMembershipDAL: Pick; + projectKeyDAL: Pick; incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; smtpService: TSmtpService; @@ -61,6 +69,8 @@ export const orgServiceFactory = ({ permissionService, smtpService, projectDAL, + projectMembershipDAL, + projectKeyDAL, tokenService, orgBotDAL, licenseService, @@ -93,6 +103,15 @@ export const orgServiceFactory = ({ return members; }; + const findOrgMembersByUsername = 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.findOrgMembersByUsername(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 +137,55 @@ 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], + username: 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 +194,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 +231,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; @@ -155,7 +240,15 @@ export const orgServiceFactory = ({ /* * Create organization * */ - const createOrganization = async (userId: string, userEmail: string, orgName: string) => { + const createOrganization = async ({ + userId, + userEmail, + orgName + }: { + userId: string; + orgName: string; + userEmail?: string | null; + }) => { const { privateKey, publicKey } = generateAsymmetricKeyPair(); const key = generateSymmetricKey(); const { @@ -283,7 +376,7 @@ export const orgServiceFactory = ({ }); } const invitee = await orgDAL.transaction(async (tx) => { - const inviteeUser = await userDAL.findUserByEmail(inviteeEmail, tx); + const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); if (inviteeUser) { // if user already exist means its already part of infisical // Thus the signup flow is not needed anymore @@ -319,9 +412,11 @@ export const orgServiceFactory = ({ // not invited before const user = await userDAL.create( { + username: inviteeEmail, email: inviteeEmail, isAccepted: false, - authMethods: [AuthMethod.EMAIL] + authMethods: [AuthMethod.EMAIL], + isGhost: false }, tx ); @@ -352,7 +447,7 @@ export const orgServiceFactory = ({ recipients: [inviteeEmail], substitutions: { inviterFirstName: user.firstName, - inviterEmail: user.email, + inviterUsername: user.username, organizationName: org?.name, email: inviteeEmail, organizationId: org?.id.toString(), @@ -372,7 +467,7 @@ export const orgServiceFactory = ({ * magic link and issue a temporary signup token for user to complete setting up their account */ const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => { - const user = await userDAL.findUserByEmail(email); + const user = await userDAL.findUserByUsername(email); if (!user) { throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" }); } @@ -424,10 +519,50 @@ export const orgServiceFactory = ({ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); - const membership = await orgDAL.deleteMembershipById(membershipId, orgId); + const deletedMembership = await orgDAL.transaction(async (tx) => { + const orgMembership = await orgDAL.deleteMembershipById(membershipId, orgId, tx); - await licenseService.updateSubscriptionOrgMemberCount(orgId); - return membership; + if (!orgMembership.userId) { + await licenseService.updateSubscriptionOrgMemberCount(orgId); + return orgMembership; + } + + // Get all the project memberships of the user in the organization + const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId); + + // Delete all the project memberships of the user in the organization + await projectMembershipDAL.delete( + { + $in: { + id: projectMemberships.map((membership) => membership.id) + } + }, + tx + ); + + // Get all the project keys of the user in the organization + const projectKeys = await projectKeyDAL.find({ + $in: { + projectId: projectMemberships.map((membership) => membership.projectId) + }, + receiverId: orgMembership.userId + }); + + // Delete all the project keys of the user in the organization + await projectKeyDAL.delete( + { + $in: { + id: projectKeys.map((key) => key.id) + } + }, + tx + ); + + await licenseService.updateSubscriptionOrgMemberCount(orgId); + return orgMembership; + }); + + return deletedMembership; }; /* @@ -470,10 +605,12 @@ export const orgServiceFactory = ({ inviteUserToOrganization, verifyUserToOrg, updateOrg, + findOrgMembersByUsername, 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 01b3c8e37c..bd8fe2e953 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 7f342f0ae4..74abf8f210 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-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts new file mode 100644 index 0000000000..3f22b87043 --- /dev/null +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -0,0 +1,36 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; +import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { BadRequestError } from "@app/lib/errors"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; + +import { TGetPrivateKeyDTO } from "./project-bot-types"; + +export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => + infisicalSymmetricDecrypt({ + keyEncoding: bot.keyEncoding as SecretKeyEncoding, + iv: bot.iv, + tag: bot.tag, + ciphertext: bot.encryptedPrivateKey + }); + +export const getBotKeyFnFactory = (projectBotDAL: TProjectBotDALFactory) => { + const getBotKeyFn = 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" }); + + const botPrivateKey = getBotPrivateKey({ bot }); + + return decryptAsymmetric({ + ciphertext: bot.encryptedProjectKey, + privateKey: botPrivateKey, + nonce: bot.encryptedProjectKeyNonce, + publicKey: bot.sender.publicKey + }); + }; + + return getBotKeyFn; +}; diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index 5ead160f28..6e281e69d0 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -1,125 +1,93 @@ import { ForbiddenError } from "@casl/ability"; -import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; +import { ProjectVersion } 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 { generateAsymmetricKeyPair } from "@app/lib/crypto"; +import { 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 { getBotKeyFnFactory, getBotPrivateKey } from "./project-bot-fns"; +import { TFindBotByProjectIdDTO, TSetActiveStateDTO } from "./project-bot-types"; type TProjectBotServiceFactoryDep = { permissionService: Pick; + projectDAL: Pick; projectBotDAL: TProjectBotDALFactory; }; export type TProjectBotServiceFactory = ReturnType; -export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => { +export const projectBotServiceFactory = ({ + projectBotDAL, + projectDAL, + permissionService +}: TProjectBotServiceFactoryDep) => { + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); + const getBotKey = async (projectId: string) => { - const appCfg = getConfig(); - const encryptionKey = appCfg.ENCRYPTION_KEY; - const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; - - 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 - }); - } - - throw new BadRequestError({ - message: "Failed to obtain bot copy of workspace key needed for operation" - }); + return getBotKeyFn(projectId); }; - 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 +95,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 +131,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 94a2943eb9..50fec2200d 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 7423a48de7..d1b4053d0c 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 22b9937a95..8faab487e8 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, sqlNestRelationships } from "@app/lib/knex"; export type TProjectMembershipDALFactory = ReturnType; @@ -10,6 +10,108 @@ export const projectMembershipDALFactory = (db: TDbClient) => { // special query const findAllProjectMembers = async (projectId: string) => { + try { + const docs = await db(TableName.ProjectMembership) + .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .join( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .join( + TableName.ProjectUserMembershipRole, + `${TableName.ProjectUserMembershipRole}.projectMembershipId`, + `${TableName.ProjectMembership}.id` + ) + .leftJoin( + TableName.ProjectRoles, + `${TableName.ProjectUserMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .select( + db.ref("id").withSchema(TableName.ProjectMembership), + db.ref("isGhost").withSchema(TableName.Users), + db.ref("username").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"), + db.ref("role").withSchema(TableName.ProjectUserMembershipRole), + db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), + db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), + db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), + db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole) + ) + .where({ isGhost: false }); + + const members = sqlNestRelationships({ + data: docs, + parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({ + id, + userId, + projectId, + user: { email, username, firstName, lastName, id: userId, publicKey, isGhost } + }), + key: "id", + childrenMapper: [ + { + label: "roles" as const, + key: "membershipRoleId", + mapper: ({ + role, + customRoleId, + customRoleName, + customRoleSlug, + membershipRoleId, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) => ({ + id: membershipRoleId, + role, + customRoleId, + customRoleName, + customRoleSlug, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) + } + ] + }); + return members; + } catch (error) { + throw new DatabaseError({ error, name: "Find all project members" }); + } + }; + + 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 findMembershipsByUsername = async (projectId: string, usernames: string[]) => { try { const members = await db(TableName.ProjectMembership) .where({ projectId }) @@ -20,24 +122,40 @@ export const projectMembershipDALFactory = (db: TDbClient) => { `${TableName.Users}.id` ) .select( - db.ref("id").withSchema(TableName.ProjectMembership), - db.ref("projectId").withSchema(TableName.ProjectMembership), - db.ref("role").withSchema(TableName.ProjectMembership), - db.ref("roleId").withSchema(TableName.ProjectMembership), - 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 }) => ({ + selectAllTableCols(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("username").withSchema(TableName.Users) + ) + .whereIn("username", usernames) + .where({ isGhost: false }); + return members.map(({ userId, username, ...data }) => ({ ...data, - user: { email, firstName, lastName, id: data.userId, publicKey } + user: { id: userId, username } })); } catch (error) { - throw new DatabaseError({ error, name: "Find all project members" }); + throw new DatabaseError({ error, name: "Find members by email" }); } }; - return { ...projectMemberOrm, findAllProjectMembers }; + const findProjectMembershipsByUserId = async (orgId: string, userId: string) => { + try { + const memberships = await db(TableName.ProjectMembership) + .where({ userId }) + .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .where({ [`${TableName.Project}.orgId` as "orgId"]: orgId }) + .select(selectAllTableCols(TableName.ProjectMembership)); + + return memberships; + } catch (error) { + throw new DatabaseError({ error, name: "Find project memberships by user id" }); + } + }; + + return { + ...projectMemberOrm, + findAllProjectMembers, + findProjectGhostUser, + findMembershipsByUsername, + findProjectMembershipsByUserId + }; }; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 6b2123e5e7..1f751ee891 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -1,36 +1,53 @@ +/* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; -import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas"; +import { + ProjectMembershipRole, + ProjectVersion, + SecretKeyEncoding, + TableName, + TProjectMemberships +} 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"; import { TUserDALFactory } from "../user/user-dal"; import { TProjectMembershipDALFactory } from "./project-membership-dal"; import { + ProjectUserMembershipTemporaryMode, TAddUsersToWorkspaceDTO, - TDeleteProjectMembershipDTO, + TAddUsersToWorkspaceNonE2EEDTO, + TDeleteProjectMembershipOldDTO, + TDeleteProjectMembershipsDTO, TGetProjectMembershipDTO, - TInviteUserToProjectDTO, TUpdateProjectMembershipDTO } from "./project-membership-types"; +import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal"; type TProjectMembershipServiceFactoryDep = { permissionService: Pick; smtpService: TSmtpService; + projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; - userDAL: Pick; - projectRoleDAL: Pick; - orgDAL: Pick; - projectDAL: Pick; + projectUserMembershipRoleDAL: Pick; + userDAL: Pick; + projectRoleDAL: Pick; + orgDAL: Pick; + projectDAL: Pick; projectKeyDAL: Pick; licenseService: Pick; }; @@ -40,8 +57,10 @@ export type TProjectMembershipServiceFactory = ReturnType { - 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 project = await projectDAL.findById(projectId); - 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" - }); - - 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 }; - }; - - 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" }); @@ -133,7 +102,7 @@ export const projectMembershipServiceFactory = ({ if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" }); await projectMembershipDAL.transaction(async (tx) => { - await projectMembershipDAL.insertMany( + const projectMemberships = await projectMembershipDAL.insertMany( orgMembers.map(({ userId }) => ({ projectId, userId: userId as string, @@ -141,6 +110,10 @@ export const projectMembershipServiceFactory = ({ })), tx ); + await projectUserMembershipRoleDAL.insertMany( + projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })), + tx + ); const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); await projectKeyDAL.insertMany( orgMembers.map(({ userId, id }) => ({ @@ -153,68 +126,242 @@ 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 project invitation", + recipients: orgMembers.filter((i) => i.email).map((i) => i.email as string), + substitutions: { + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } + }); + } return orgMembers; }; + const addUsersToProjectNonE2EE = async ({ + projectId, + actorId, + actor, + emails, + usernames, + 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 usernamesAndEmails = [...emails, ...usernames]; + + const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [ + ...new Set(usernamesAndEmails.map((element) => element.toLowerCase())) + ]); + + if (orgMembers.length !== usernamesAndEmails.length) + throw new BadRequestError({ message: "Some users are not part of org" }); + + if (!orgMembers.length) return []; + + 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 projectMemberships = await projectMembershipDAL.insertMany( + orgMembers.map(({ user }) => ({ + projectId, + userId: user.id, + role: ProjectMembershipRole.Member + })), + tx + ); + await projectUserMembershipRoleDAL.insertMany( + projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })), + tx + ); + + members.push(...projectMemberships); + + 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 recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string); + + const appCfg = getConfig(); + + if (recipients.length) { + await smtpService.sendMail({ + template: SmtpTemplates.WorkspaceInvite, + subjectLine: "Infisical project invitation", + recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string), + substitutions: { + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } + }); + } + } + return members; + }; + const updateProjectMembership = async ({ actorId, actor, actorOrgId, projectId, membershipId, - role + roles }: TUpdateProjectMembershipDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); - const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); - if (isCustomRole) { - const customRole = await projectRoleDAL.findOne({ slug: role, projectId }); - if (!customRole) throw new BadRequestError({ name: "Update project membership", message: "Role not found" }); - const project = await projectDAL.findById(customRole.projectId); - const plan = await licenseService.getPlan(project.orgId); + const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); + if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) { + throw new BadRequestError({ + message: "Unauthorized member update", + name: "Update project membership" + }); + } + + // validate custom roles input + const customInputRoles = roles.filter( + ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) + ); + const hasCustomRole = Boolean(customInputRoles.length); + if (hasCustomRole) { + const plan = await licenseService.getPlan(actorOrgId as string); if (!plan?.rbac) throw new BadRequestError({ message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member." }); - - const [membership] = await projectMembershipDAL.update( - { id: membershipId, projectId }, - { - role: ProjectMembershipRole.Custom, - roleId: customRole.id - } - ); - return membership; } - const [membership] = await projectMembershipDAL.update({ id: membershipId, projectId }, { role, roleId: null }); - return membership; + const customRoles = hasCustomRole + ? await projectRoleDAL.find({ + projectId, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" }); + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const santiziedProjectMembershipRoles = roles.map((inputRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); + if (!inputRole.isTemporary) { + return { + projectMembershipId: membershipId, + role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, + customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null + }; + } + + // check cron or relative here later for now its just relative + const relativeTimeInMs = ms(inputRole.temporaryRange); + return { + projectMembershipId: membershipId, + role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, + customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, + isTemporary: true, + temporaryMode: ProjectUserMembershipTemporaryMode.Relative, + temporaryRange: inputRole.temporaryRange, + temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) + }; + }); + + const updatedRoles = await projectMembershipDAL.transaction(async (tx) => { + await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx); + return projectUserMembershipRoleDAL.insertMany(santiziedProjectMembershipRoles, tx); + }); + + return updatedRoles; }; + // 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 +370,78 @@ export const projectMembershipServiceFactory = ({ return membership; }; + const deleteProjectMemberships = async ({ + actorId, + actor, + actorOrgId, + projectId, + emails, + usernames + }: 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" + }); + } + + const usernamesAndEmails = [...emails, ...usernames]; + + const projectMembers = await projectMembershipDAL.findMembershipsByUsername(projectId, [ + ...new Set(usernamesAndEmails.map((element) => element.toLowerCase())) + ]); + + if (projectMembers.length !== usernamesAndEmails.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 6ee1d29657..2ba245c8c3 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -1,24 +1,51 @@ import { TProjectPermission } from "@app/lib/types"; export type TGetProjectMembershipDTO = TProjectPermission; +export enum ProjectUserMembershipTemporaryMode { + Relative = "relative" +} export type TInviteUserToProjectDTO = { - email: string; + emails: string[]; } & TProjectPermission; export type TUpdateProjectMembershipDTO = { membershipId: string; - role: string; + roles: ( + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: ProjectUserMembershipTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } + )[]; } & TProjectPermission; -export type TDeleteProjectMembershipDTO = { +export type TDeleteProjectMembershipOldDTO = { membershipId: string; } & TProjectPermission; +export type TDeleteProjectMembershipsDTO = { + emails: string[]; + usernames: string[]; +} & TProjectPermission; + export type TAddUsersToWorkspaceDTO = { + sendEmails?: boolean; members: { orgMembershipId: string; workspaceEncryptedKey: string; workspaceEncryptedNonce: string; }[]; } & TProjectPermission; + +export type TAddUsersToWorkspaceNonE2EEDTO = { + sendEmails?: boolean; + emails: string[]; + usernames: string[]; +} & TProjectPermission; diff --git a/backend/src/services/project-membership/project-user-membership-role-dal.ts b/backend/src/services/project-membership/project-user-membership-role-dal.ts new file mode 100644 index 0000000000..b1cb55b9be --- /dev/null +++ b/backend/src/services/project-membership/project-user-membership-role-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 TProjectUserMembershipRoleDALFactory = ReturnType; + +export const projectUserMembershipRoleDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ProjectUserMembershipRole); + return orm; +}; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 7da98b314d..b45a6e8a52 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -76,7 +76,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId }); - if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); + if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); return deletedRole; }; @@ -92,7 +92,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: name: "Admin", slug: ProjectMembershipRole.Admin, description: "Complete administration access over the project", - permissions: packRules(projectAdminPermissions.rules), + permissions: packRules(projectAdminPermissions), createdAt: new Date(), updatedAt: new Date() }, @@ -102,7 +102,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: name: "Developer", slug: ProjectMembershipRole.Member, description: "Non-administrative role in an project", - permissions: packRules(projectMemberPermissions.rules), + permissions: packRules(projectMemberPermissions), createdAt: new Date(), updatedAt: new Date() }, @@ -112,7 +112,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: name: "Viewer", slug: ProjectMembershipRole.Viewer, description: "Non-administrative role in an project", - permissions: packRules(projectViewerPermission.rules), + permissions: packRules(projectViewerPermission), createdAt: new Date(), updatedAt: new Date() }, @@ -122,7 +122,7 @@ export const projectRoleServiceFactory = ({ projectRoleDAL, permissionService }: name: "No Access", slug: "no-access", description: "No access to any resources in the project", - permissions: packRules(projectNoAccessPermissions.rules), + permissions: packRules(projectNoAccessPermissions), createdAt: new Date(), updatedAt: new Date() }, diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 44ba574819..7d0826e129 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 0000000000..3ac75248df --- /dev/null +++ b/backend/src/services/project/project-fns.ts @@ -0,0 +1,51 @@ +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; + plainProjectKey?: string; +}; + +export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCreateProjectKeyDTO) => { + // 3. Create a random key that we'll use as the project key. + const randomBytes = plainProjectKey || 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 0000000000..4431855ce7 --- /dev/null +++ b/backend/src/services/project/project-queue.ts @@ -0,0 +1,574 @@ +/* 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 { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-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; + projectUserMembershipRoleDAL: 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, + projectUserMembershipRoleDAL +}: 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) { + throw new Error("Project not found"); + } + if (!oldProjectKey) { + throw new Error("Old 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 decryptedPlainProjectKey = decryptAsymmetric({ + ciphertext: oldProjectKey.encryptedKey, + nonce: oldProjectKey.nonce, + publicKey: oldProjectKey.sender.publicKey, + privateKey: userPrivateKey + }); + + 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({ + plainProjectKey: decryptedPlainProjectKey, + 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 + const projectMembership = await projectMembershipDAL.create( + { + projectId: project.id, + userId: ghostUser.user.id, + role: ProjectMembershipRole.Admin + }, + tx + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: projectMembership.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) { + throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`); + } + + if (!orgMembership) { + // This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case. + logger.info("User is not in organization", { + userId: key.receiverId, + orgId: project.orgId, + projectId: project.id + }); + // eslint-disable-next-line no-continue + continue; + } + + 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("Failed to upgrade project", err, { + extra: { + project, + jobData: data + } + }); + } + + 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 387c9bd78e..873a7d36f8 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,22 +1,42 @@ 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 { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-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 { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-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 +46,20 @@ 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; + identityProjectMembershipRoleDAL: Pick; + projectKeyDAL: Pick; + projectBotDAL: Pick; + projectMembershipDAL: Pick; + projectUserMembershipRoleDAL: Pick; secretBlindIndexDAL: Pick; permissionService: TPermissionServiceFactory; + orgService: Pick; licenseService: Pick; }; @@ -38,18 +67,32 @@ export type TProjectServiceFactory = ReturnType; export const projectServiceFactory = ({ projectDAL, + projectQueue, + projectKeyDAL, permissionService, + userDAL, folderDAL, + orgService, + identityProjectDAL, + projectBotDAL, + identityOrgMembershipDAL, secretBlindIndexDAL, projectMembershipDAL, projectEnvDAL, - licenseService + licenseService, + projectUserMembershipRoleDAL, + identityProjectMembershipRoleDAL }: TProjectServiceFactoryDep) => { /* * 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 +107,32 @@ 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)}`) }, - tx - ); - // set user as admin member for proeject - await projectMembershipDAL.create( { - userId: actorId, - role: ProjectMembershipRole.Admin, - projectId: project.id + name: workspaceName, + orgId, + slug: slug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), + version: ProjectVersion.V2 }, tx ); + // set ghost user as admin of project + const projectMembership = await projectMembershipDAL.create( + { + userId: ghostUser.user.id, + projectId: project.id, + role: ProjectMembershipRole.Admin + }, + tx + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, + tx + ); + // generate the blind index for project await secretBlindIndexDAL.create( { @@ -99,18 +154,178 @@ 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 + const userProjectMembership = await projectMembershipDAL.create( + { + projectId: project.id, + userId: user.id, + role: projectAdmin.projectRole + }, + tx + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: userProjectMembership.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); + + const identityProjectMembership = await identityProjectDAL.create( + { + identityId: actorId, + projectId: project.id, + role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin, + roleId: customRole?.id + }, + tx + ); + + await identityProjectMembershipRoleDAL.create( + { + projectMembershipId: identityProjectMembership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin, + customRoleId: 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 +339,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 +372,55 @@ export const projectServiceFactory = ({ return updatedProject; }; + const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => { + const { permission, hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); + + if (!hasRole(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 2ffea11177..3843450c28 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-blind-index/secret-blind-index-service.ts b/backend/src/services/secret-blind-index/secret-blind-index-service.ts index da7a44df2b..b681266fd9 100644 --- a/backend/src/services/secret-blind-index/secret-blind-index-service.ts +++ b/backend/src/services/secret-blind-index/secret-blind-index-service.ts @@ -37,8 +37,8 @@ export const secretBlindIndexServiceFactory = ({ }; const getProjectSecrets = async ({ projectId, actorId, actor }: TGetProjectSecretsDTO) => { - const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId); - if (membership?.role !== ProjectMembershipRole.Admin) { + const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId); + if (!hasRole(ProjectMembershipRole.Admin)) { throw new UnauthorizedError({ message: "User must be admin" }); } @@ -53,8 +53,8 @@ export const secretBlindIndexServiceFactory = ({ actorOrgId, secretsToUpdate }: TUpdateProjectSecretNameDTO) => { - const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); - if (membership?.role !== ProjectMembershipRole.Admin) { + const { hasRole } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); + if (!hasRole(ProjectMembershipRole.Admin)) { throw new UnauthorizedError({ message: "User must be admin" }); } diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 53d2a9fdf0..26c1c1f4f0 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; import path from "path"; -import { v4 as uuidv4 } from "uuid"; +import { v4 as uuidv4, validate as uuidValidate } from "uuid"; import { TSecretFoldersInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -164,7 +164,7 @@ export const secretFolderServiceFactory = ({ actorOrgId, environment, path: secretPath, - id + idOrName }: TDeleteFolderDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan( @@ -179,7 +179,10 @@ export const secretFolderServiceFactory = ({ const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, tx); if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); - const [doc] = await folderDAL.delete({ envId: env.id, id, parentId: parentFolder.id }, tx); + const [doc] = await folderDAL.delete( + { envId: env.id, [uuidValidate(idOrName) ? "id" : "name"]: idOrName, parentId: parentFolder.id }, + tx + ); if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Delete folder" }); return doc; }); diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index 7a68434f53..88b7b1017a 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -16,7 +16,7 @@ export type TUpdateFolderDTO = { export type TDeleteFolderDTO = { environment: string; path: string; - id: string; + idOrName: string; } & TProjectPermission; export type TGetFolderDTO = { diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index 1fa55f214e..00b33a13c8 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 a519c78209..1beae9be69 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-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index 62ebd8a239..1007ec4c3d 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -19,7 +19,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); - const existingTag = await secretTagDAL.findOne({ slug }); + const existingTag = await secretTagDAL.findOne({ slug, projectId }); if (existingTag) throw new BadRequestError({ message: "Tag already exist" }); const newTag = await secretTagDAL.create({ diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index ba65033cbe..11cd522ca2 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-fns.ts b/backend/src/services/secret/secret-fns.ts index 0f6caa2489..212bb01f07 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,12 +1,35 @@ /* eslint-disable no-await-in-loop */ import path from "path"; -import { SecretKeyEncoding, TSecretBlindIndexes, TSecrets } from "@app/db/schemas"; +import { + SecretEncryptionAlgo, + SecretKeyEncoding, + SecretType, + TableName, + TSecretBlindIndexes, + TSecrets +} from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { buildSecretBlindIndexFromName, decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { + buildSecretBlindIndexFromName, + decryptSymmetric128BitHexKeyUTF8, + encryptSymmetric128BitHexKeyUTF8 +} from "@app/lib/crypto"; +import { BadRequestError } from "@app/lib/errors"; +import { groupBy, unique } from "@app/lib/fn"; +import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretDALFactory } from "./secret-dal"; +import { + TCreateManySecretsRawFn, + TCreateManySecretsRawFnFactory, + TFnSecretBlindIndexCheck, + TFnSecretBulkInsert, + TFnSecretBulkUpdate, + TUpdateManySecretsRawFn, + TUpdateManySecretsRawFnFactory +} from "./secret-types"; export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { const appCfg = getConfig(); @@ -228,3 +251,399 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ user: secret.userId }; }; + +/** + * Checks and handles secrets using a blind index method. + * The function generates mappings between secret names and their blind indexes, validates user IDs for personal secrets, and retrieves secrets from the database based on their blind indexes. + * For new secrets (isNew = true), it ensures they don't already exist in the database. + * For existing secrets, it verifies their presence in the database. + * If discrepancies are found, errors are thrown. The function returns mappings and the fetched secrets. + */ +export const fnSecretBlindIndexCheck = async ({ + inputSecrets, + folderId, + isNew, + userId, + blindIndexCfg, + secretDAL +}: TFnSecretBlindIndexCheck) => { + const blindIndex2KeyName: Record = {}; // used at audit log point + const keyName2BlindIndex = await Promise.all( + inputSecrets.map(({ secretName }) => generateSecretBlindIndexBySalt(secretName, blindIndexCfg)) + ).then((blindIndexes) => + blindIndexes.reduce>((prev, curr, i) => { + // eslint-disable-next-line + prev[inputSecrets[i].secretName] = curr; + blindIndex2KeyName[curr] = inputSecrets[i].secretName; + return prev; + }, {}) + ); + + if (inputSecrets.some(({ type }) => type === SecretType.Personal) && !userId) { + throw new BadRequestError({ message: "Missing user id for personal secret" }); + } + + const secrets = await secretDAL.findByBlindIndexes( + folderId, + inputSecrets.map(({ secretName, type }) => ({ + blindIndex: keyName2BlindIndex[secretName], + type: type || SecretType.Shared + })), + userId + ); + + if (isNew) { + if (secrets.length) throw new BadRequestError({ message: "Secret already exist" }); + } else { + const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map( + (el) => blindIndex2KeyName[el.secretBlindIndex as string] + ); + const hasUnknownSecretsProvided = secretKeysInDB.length !== inputSecrets.length; + if (hasUnknownSecretsProvided) { + const keysMissingInDB = Object.keys(keyName2BlindIndex).filter((key) => !secretKeysInDB.includes(key)); + throw new BadRequestError({ + message: `Secret not found: blind index ${keysMissingInDB.join(",")}` + }); + } + } + + return { blindIndex2KeyName, keyName2BlindIndex, secrets }; +}; + +// these functions are special functions shared by a couple of resources +// used by secret approval, rotation or anywhere in which secret needs to modified +export const fnSecretBulkInsert = async ({ + // TODO: Pick types here + folderId, + inputSecrets, + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + tx +}: TFnSecretBulkInsert) => { + const newSecrets = await secretDAL.insertMany( + inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })), + tx + ); + const newSecretGroupByBlindIndex = groupBy(newSecrets, (item) => item.secretBlindIndex as string); + const newSecretTags = inputSecrets.flatMap(({ tags: secretTags = [], secretBlindIndex }) => + secretTags.map((tag) => ({ + [`${TableName.SecretTag}Id` as const]: tag, + [`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id + })) + ); + const secretVersions = await secretVersionDAL.insertMany( + inputSecrets.map(({ tags, ...el }) => ({ + ...el, + folderId, + secretId: newSecretGroupByBlindIndex[el.secretBlindIndex as string][0].id + })), + tx + ); + if (newSecretTags.length) { + const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx); + const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); + const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({ + [`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id, + [`${TableName.SecretTag}Id` as const]: secret_tagsId + })); + await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); + } + + return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); +}; + +export const fnSecretBulkUpdate = async ({ + tx, + inputSecrets, + folderId, + projectId, + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL +}: TFnSecretBulkUpdate) => { + const newSecrets = await secretDAL.bulkUpdate( + inputSecrets.map(({ filter, data: { tags, ...data } }) => ({ + filter: { ...filter, folderId }, + data + })), + tx + ); + const secretVersions = await secretVersionDAL.insertMany( + newSecrets.map(({ id, createdAt, updatedAt, ...el }) => ({ + ...el, + secretId: id + })), + tx + ); + const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) => + tags !== undefined ? { tags, secretId: newSecrets[i].id } : [] + ); + if (secsUpdatedTag.length) { + await secretTagDAL.deleteTagsManySecret( + projectId, + secsUpdatedTag.map(({ secretId }) => secretId), + tx + ); + const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], secretId }) => + secretTags.map((tag) => ({ + [`${TableName.SecretTag}Id` as const]: tag, + [`${TableName.Secret}Id` as const]: secretId + })) + ); + if (newSecretTags.length) { + const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx); + const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); + const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({ + [`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id, + [`${TableName.SecretTag}Id` as const]: secret_tagsId + })); + await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); + } + } + + return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); +}; + +export const createManySecretsRawFnFactory = ({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL +}: TCreateManySecretsRawFnFactory) => { + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); + const createManySecretsRawFn = async ({ + projectId, + environment, + path: secretPath, + secrets, + userId + }: TCreateManySecretsRawFn) => { + const botKey = await getBotKeyFn(projectId); + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + + await projectDAL.checkProjectUpgradeStatus(projectId); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); + const folderId = folder.id; + + const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); + if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" }); + + // insert operation + const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({ + inputSecrets: secrets, + folderId, + isNew: true, + blindIndexCfg, + secretDAL + }); + + const inputSecrets = await Promise.all( + secrets.map(async (secret) => { + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey); + + if (secret.type === SecretType.Personal) { + if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" }); + const sharedExist = await secretDAL.findOne({ + secretBlindIndex: keyName2BlindIndex[secret.secretName], + folderId, + type: SecretType.Shared + }); + + if (!sharedExist) + throw new BadRequestError({ + message: "Failed to create personal secret override for no corresponding shared secret" + }); + } + + const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : []; + if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); + + return { + type: secret.type, + userId: secret.type === SecretType.Personal ? userId : null, + secretName: secret.secretName, + 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, + skipMultilineEncoding: secret.skipMultilineEncoding, + tags: secret.tags + }; + }) + ); + + const newSecrets = await secretDAL.transaction(async (tx) => + fnSecretBulkInsert({ + inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({ + ...el, + version: 0, + secretBlindIndex: keyName2BlindIndex[secretName], + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.UTF8 + })), + folderId, + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + tx + }) + ); + + return newSecrets; + }; + + return createManySecretsRawFn; +}; + +export const updateManySecretsRawFnFactory = ({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL +}: TUpdateManySecretsRawFnFactory) => { + const getBotKeyFn = getBotKeyFnFactory(projectBotDAL); + const updateManySecretsRawFn = async ({ + projectId, + environment, + path: secretPath, + secrets, // consider accepting instead ciphertext secrets + userId + }: TUpdateManySecretsRawFn): Promise> => { + const botKey = await getBotKeyFn(projectId); + if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + + await projectDAL.checkProjectUpgradeStatus(projectId); + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" }); + const folderId = folder.id; + + const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); + if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" }); + + const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({ + inputSecrets: secrets, + folderId, + isNew: false, + blindIndexCfg, + secretDAL, + userId + }); + + const inputSecrets = await Promise.all( + secrets.map(async (secret) => { + if (secret.newSecretName === "") { + throw new BadRequestError({ message: "New secret name cannot be empty" }); + } + + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey); + + if (secret.type === SecretType.Personal) { + if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" }); + + const sharedExist = await secretDAL.findOne({ + secretBlindIndex: keyName2BlindIndex[secret.secretName], + folderId, + type: SecretType.Shared + }); + + if (!sharedExist) + throw new BadRequestError({ + message: "Failed to update personal secret override for no corresponding shared secret" + }); + + if (secret.newSecretName) + throw new BadRequestError({ message: "Personal secret cannot change the key name" }); + } + + const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : []; + if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); + + return { + type: secret.type, + userId: secret.type === SecretType.Personal ? userId : null, + secretName: secret.secretName, + newSecretName: secret.newSecretName, + 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, + skipMultilineEncoding: secret.skipMultilineEncoding, + tags: secret.tags + }; + }) + ); + + const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags); + const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : []; + if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); + + // now find any secret that needs to update its name + // same process as above + const nameUpdatedSecrets = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName)); + const { keyName2BlindIndex: newKeyName2BlindIndex } = await fnSecretBlindIndexCheck({ + inputSecrets: nameUpdatedSecrets, + folderId, + isNew: true, + blindIndexCfg, + secretDAL + }); + + const updatedSecrets = await secretDAL.transaction(async (tx) => + fnSecretBulkUpdate({ + folderId, + projectId, + tx, + inputSecrets: inputSecrets.map(({ secretName, newSecretName, ...el }) => ({ + filter: { secretBlindIndex: keyName2BlindIndex[secretName], type: SecretType.Shared }, + data: { + ...el, + folderId, + secretBlindIndex: + newSecretName && newKeyName2BlindIndex[newSecretName] + ? newKeyName2BlindIndex[newSecretName] + : keyName2BlindIndex[secretName], + algorithm: SecretEncryptionAlgo.AES_256_GCM, + keyEncoding: SecretKeyEncoding.UTF8 + } + })), + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL + }) + ); + + return updatedSecrets; + }; + + return updateManySecretsRawFn; +}; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index b797b7caf6..2e5ea7f93f 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -6,6 +6,12 @@ import { BadRequestError } from "@app/lib/errors"; import { isSamePath } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; +import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns"; +import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; +import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; +import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; +import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service"; @@ -29,18 +35,23 @@ export type TSecretQueueFactory = ReturnType; type TSecretQueueFactoryDep = { queueService: TQueueServiceFactory; - integrationDAL: Pick; + integrationDAL: Pick; projectBotService: Pick; integrationAuthService: Pick; - folderDAL: Pick; - secretDAL: Pick; + folderDAL: TSecretFolderDALFactory; + secretDAL: TSecretDALFactory; secretImportDAL: Pick; webhookDAL: Pick; projectEnvDAL: Pick; - projectDAL: Pick; + projectDAL: TProjectDALFactory; + projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: Pick; smtpService: TSmtpService; orgDAL: Pick; + secretVersionDAL: TSecretVersionDALFactory; + secretBlindIndexDAL: TSecretBlindIndexDALFactory; + secretTagDAL: TSecretTagDALFactory; + secretVersionTagDAL: TSecretVersionTagDALFactory; }; export type TGetSecrets = { @@ -62,8 +73,35 @@ export const secretQueueFactory = ({ orgDAL, smtpService, projectDAL, - projectMembershipDAL + projectBotDAL, + projectMembershipDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL }: TSecretQueueFactoryDep) => { + const createManySecretsRawFn = createManySecretsRawFnFactory({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL + }); + + const updateManySecretsRawFn = updateManySecretsRawFnFactory({ + projectDAL, + projectBotDAL, + secretDAL, + secretVersionDAL, + secretBlindIndexDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL + }); + const syncIntegrations = async (dto: TGetSecrets) => { await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { attempts: 5, @@ -307,6 +345,9 @@ export const secretQueueFactory = ({ } await syncIntegrationSecrets({ + createManySecretsRawFn, + updateManySecretsRawFn, + integrationDAL, integration, integrationAuth, secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, @@ -350,7 +391,7 @@ export const secretQueueFactory = ({ await smtpService.sendMail({ template: SmtpTemplates.SecretReminder, subjectLine: "Infisical secret reminder", - recipients: [...projectMembers.map((m) => m.user.email)], + recipients: [...projectMembers.map((m) => m.user.email)].filter((email) => email).map((email) => email as string), substitutions: { reminderNote: data.note, // May not be present. projectName: project.name, diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index ff9dce9a8d..51136e40f5 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType, TableName } from "@app/db/schemas"; +import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; @@ -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"; @@ -18,7 +19,7 @@ import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretDALFactory } from "./secret-dal"; -import { decryptSecretRaw, generateSecretBlindIndexBySalt } from "./secret-fns"; +import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; import { TSecretQueueFactory } from "./secret-queue"; import { TCreateBulkSecretDTO, @@ -27,11 +28,8 @@ import { TDeleteBulkSecretDTO, TDeleteSecretDTO, TDeleteSecretRawDTO, - TFnSecretBlindIndexCheck, TFnSecretBlindIndexCheckV2, TFnSecretBulkDelete, - TFnSecretBulkInsert, - TFnSecretBulkUpdate, TGetASecretDTO, TGetASecretRawDTO, TGetSecretsDTO, @@ -49,6 +47,7 @@ type TSecretServiceFactoryDep = { secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; folderDAL: Pick; + projectDAL: Pick; secretBlindIndexDAL: TSecretBlindIndexDALFactory; permissionService: Pick; snapshotService: Pick; @@ -68,6 +67,7 @@ export const secretServiceFactory = ({ permissionService, snapshotService, secretQueueService, + projectDAL, projectBotService, secretImportDAL, secretVersionTagDAL @@ -92,85 +92,6 @@ export const secretServiceFactory = ({ return secretBlindIndex; }; - // these functions are special functions shared by a couple of resources - // used by secret approval, rotation or anywhere in which secret needs to modified - const fnSecretBulkInsert = async ({ folderId, inputSecrets, tx }: TFnSecretBulkInsert) => { - const newSecrets = await secretDAL.insertMany( - inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })), - tx - ); - const newSecretGroupByBlindIndex = groupBy(newSecrets, (item) => item.secretBlindIndex as string); - const newSecretTags = inputSecrets.flatMap(({ tags: secretTags = [], secretBlindIndex }) => - secretTags.map((tag) => ({ - [`${TableName.SecretTag}Id` as const]: tag, - [`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id - })) - ); - const secretVersions = await secretVersionDAL.insertMany( - inputSecrets.map(({ tags, ...el }) => ({ - ...el, - folderId, - secretId: newSecretGroupByBlindIndex[el.secretBlindIndex as string][0].id - })), - tx - ); - if (newSecretTags.length) { - const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx); - const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); - const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({ - [`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id, - [`${TableName.SecretTag}Id` as const]: secret_tagsId - })); - await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); - } - - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); - }; - - const fnSecretBulkUpdate = async ({ tx, inputSecrets, folderId, projectId }: TFnSecretBulkUpdate) => { - const newSecrets = await secretDAL.bulkUpdate( - inputSecrets.map(({ filter, data: { tags, ...data } }) => ({ - filter: { ...filter, folderId }, - data - })), - tx - ); - const secretVersions = await secretVersionDAL.insertMany( - newSecrets.map(({ id, createdAt, updatedAt, ...el }) => ({ - ...el, - secretId: id - })), - tx - ); - const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) => - tags !== undefined ? { tags, secretId: newSecrets[i].id } : [] - ); - if (secsUpdatedTag.length) { - await secretTagDAL.deleteTagsManySecret( - projectId, - secsUpdatedTag.map(({ secretId }) => secretId), - tx - ); - const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], secretId }) => - secretTags.map((tag) => ({ - [`${TableName.SecretTag}Id` as const]: tag, - [`${TableName.Secret}Id` as const]: secretId - })) - ); - if (newSecretTags.length) { - const secTags = await secretTagDAL.saveTagsToSecret(newSecretTags, tx); - const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); - const newSecretVersionTags = secTags.flatMap(({ secretsId, secret_tagsId }) => ({ - [`${TableName.SecretVersion}Id` as const]: secVersionsGroupBySecId[secretsId][0].id, - [`${TableName.SecretTag}Id` as const]: secret_tagsId - })); - await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); - } - } - - return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); - }; - const fnSecretBulkDelete = async ({ folderId, inputSecrets, tx, actorId }: TFnSecretBulkDelete) => { const deletedSecrets = await secretDAL.deleteMany( inputSecrets.map(({ type, secretBlindIndex }) => ({ @@ -199,54 +120,6 @@ export const secretServiceFactory = ({ return deletedSecrets; }; - // this is a utility function for secret modification - // this will check given secret name blind index exist or not - // if its a created secret set isNew to true - // thus if these blindindex exist it will throw an error - // vice versa when u need to check for updated secret - // this will also return the blind index grouped by secretName - const fnSecretBlindIndexCheck = async ({ - inputSecrets, - folderId, - isNew, - userId, - blindIndexCfg - }: TFnSecretBlindIndexCheck) => { - const blindIndex2KeyName: Record = {}; // used at audit log point - const keyName2BlindIndex = await Promise.all( - inputSecrets.map(({ secretName }) => generateSecretBlindIndexBySalt(secretName, blindIndexCfg)) - ).then((blindIndexes) => - blindIndexes.reduce>((prev, curr, i) => { - // eslint-disable-next-line - prev[inputSecrets[i].secretName] = curr; - blindIndex2KeyName[curr] = inputSecrets[i].secretName; - return prev; - }, {}) - ); - - if (inputSecrets.some(({ type }) => type === SecretType.Personal) && !userId) { - throw new BadRequestError({ message: "Missing user id for personal secret" }); - } - - const secrets = await secretDAL.findByBlindIndexes( - folderId, - inputSecrets.map(({ secretName, type }) => ({ - blindIndex: keyName2BlindIndex[secretName], - type: type || SecretType.Shared - })), - userId - ); - - if (isNew) { - if (secrets.length) throw new BadRequestError({ message: "Secret already exist" }); - } else if (secrets.length !== inputSecrets.length) - throw new BadRequestError({ - message: `Secret not found: blind index ${JSON.stringify(keyName2BlindIndex)}` - }); - - return { blindIndex2KeyName, keyName2BlindIndex, secrets }; - }; - // this is used when secret blind index already exist // mainly for secret approval const fnSecretBlindIndexCheckV2 = async ({ inputSecrets, folderId, userId }: TFnSecretBlindIndexCheckV2) => { @@ -281,6 +154,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; @@ -297,7 +172,8 @@ export const secretServiceFactory = ({ folderId, isNew: true, userId: actorId, - blindIndexCfg + blindIndexCfg, + secretDAL }); // if user creating personal check its shared also exist @@ -334,6 +210,10 @@ export const secretServiceFactory = ({ tags: inputSecret.tags } ], + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, tx }) ); @@ -359,6 +239,12 @@ export const secretServiceFactory = ({ subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); + await projectDAL.checkProjectUpgradeStatus(projectId); + + if (inputSecret.newSecretName === "") { + throw new BadRequestError({ message: "New secret name cannot be empty" }); + } + const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); const folderId = folder.id; @@ -375,7 +261,8 @@ export const secretServiceFactory = ({ folderId, isNew: false, blindIndexCfg, - userId: actorId + userId: actorId, + secretDAL }); if (inputSecret.newSecretName && inputSecret.type === SecretType.Personal) { throw new BadRequestError({ message: "Personal secret cannot change the key name" }); @@ -387,7 +274,8 @@ export const secretServiceFactory = ({ inputSecrets: [{ secretName: inputSecret.newSecretName }], folderId, isNew: true, - blindIndexCfg + blindIndexCfg, + secretDAL }); newSecretNameBlindIndex = kN2NewBlindIndex[inputSecret.newSecretName]; } @@ -434,6 +322,10 @@ export const secretServiceFactory = ({ } } ], + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, tx }) ); @@ -459,6 +351,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; @@ -474,7 +368,8 @@ export const secretServiceFactory = ({ inputSecrets: [{ secretName: inputSecret.secretName }], folderId, isNew: false, - blindIndexCfg + blindIndexCfg, + secretDAL }); const deletedSecret = await secretDAL.transaction(async (tx) => @@ -650,18 +545,21 @@ 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; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); - if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" }); + if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" }); const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({ inputSecrets, folderId, isNew: true, - blindIndexCfg + blindIndexCfg, + secretDAL }); // get all tags @@ -680,6 +578,10 @@ export const secretServiceFactory = ({ keyEncoding: SecretKeyEncoding.UTF8 })), folderId, + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, tx }) ); @@ -705,8 +607,10 @@ 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" }); + if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update secret" }); const folderId = folder.id; const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); @@ -716,7 +620,8 @@ export const secretServiceFactory = ({ inputSecrets, folderId, isNew: false, - blindIndexCfg + blindIndexCfg, + secretDAL }); // now find any secret that needs to update its name @@ -726,7 +631,8 @@ export const secretServiceFactory = ({ inputSecrets: nameUpdatedSecrets, folderId, isNew: true, - blindIndexCfg + blindIndexCfg, + secretDAL }); // get all tags @@ -751,7 +657,11 @@ export const secretServiceFactory = ({ algorithm: SecretEncryptionAlgo.AES_256_GCM, keyEncoding: SecretKeyEncoding.UTF8 } - })) + })), + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL }) ); @@ -776,6 +686,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; @@ -787,7 +699,8 @@ export const secretServiceFactory = ({ inputSecrets, folderId, isNew: false, - blindIndexCfg + blindIndexCfg, + secretDAL }); const secretsDeleted = await secretDAL.transaction(async (tx) => diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 50f678172c..7ad4d65d72 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -2,6 +2,14 @@ import { Knex } from "knex"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; +import { TSecretDALFactory } from "@app/services/secret/secret-dal"; +import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; +import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; +import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; type TPartialSecret = Pick; @@ -181,12 +189,20 @@ export type TFnSecretBulkInsert = { folderId: string; tx?: Knex; inputSecrets: Array & { tags?: string[] }>; + secretDAL: Pick; + secretVersionDAL: Pick; + secretTagDAL: Pick; + secretVersionTagDAL: Pick; }; export type TFnSecretBulkUpdate = { folderId: string; projectId: string; inputSecrets: { filter: Partial; data: TSecretsUpdate & { tags?: string[] } }[]; + secretDAL: Pick; + secretVersionDAL: Pick; + secretTagDAL: Pick; + secretVersionTagDAL: Pick; tx?: Knex; }; @@ -204,6 +220,7 @@ export type TFnSecretBlindIndexCheck = { blindIndexCfg: TSecretBlindIndexes; inputSecrets: Array<{ secretName: string; type?: SecretType }>; isNew: boolean; + secretDAL: Pick; }; // when blind index is already present @@ -229,3 +246,66 @@ export type TRemoveSecretReminderDTO = { secretId: string; repeatDays: number; }; + +// --- + +export type TCreateManySecretsRawFnFactory = { + projectDAL: TProjectDALFactory; + projectBotDAL: TProjectBotDALFactory; + secretDAL: TSecretDALFactory; + secretVersionDAL: TSecretVersionDALFactory; + secretBlindIndexDAL: TSecretBlindIndexDALFactory; + secretTagDAL: TSecretTagDALFactory; + secretVersionTagDAL: TSecretVersionTagDALFactory; + folderDAL: TSecretFolderDALFactory; +}; + +export type TCreateManySecretsRawFn = { + projectId: string; + environment: string; + path: string; + secrets: { + secretName: string; + secretValue: string; + type: SecretType; + secretComment?: string; + skipMultilineEncoding?: boolean; + tags?: string[]; + metadata?: { + source?: string; + }; + }[]; + userId?: string; // only relevant for personal secret(s) +}; + +export type TUpdateManySecretsRawFnFactory = { + projectDAL: TProjectDALFactory; + projectBotDAL: TProjectBotDALFactory; + secretDAL: TSecretDALFactory; + secretVersionDAL: TSecretVersionDALFactory; + secretBlindIndexDAL: TSecretBlindIndexDALFactory; + secretTagDAL: TSecretTagDALFactory; + secretVersionTagDAL: TSecretVersionTagDALFactory; + folderDAL: TSecretFolderDALFactory; +}; + +export type TUpdateManySecretsRawFn = { + projectId: string; + environment: string; + path: string; + secrets: { + secretName: string; + newSecretName?: string; + secretValue: string; + type: SecretType; + secretComment?: string; + skipMultilineEncoding?: boolean; + secretReminderRepeatDays?: number | null; + secretReminderNote?: string | null; + tags?: string[]; + metadata?: { + source?: string; + }; + }[]; + userId?: string; +}; diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 7a6695e187..758352ed24 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/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index b94c7ee352..5d3fcc5c80 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -1,10 +1,32 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TUsers } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TServiceTokenDALFactory = ReturnType; export const serviceTokenDALFactory = (db: TDbClient) => { const stOrm = ormify(db, TableName.ServiceToken); - return stOrm; + + const findById = async (id: string, tx?: Knex) => { + try { + const doc = await (tx || db)(TableName.ServiceToken) + .leftJoin( + TableName.Users, + `${TableName.Users}.id`, + db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) + ) + .where(`${TableName.ServiceToken}.id`, id) + .select(selectAllTableCols(TableName.ServiceToken)) + .select(db.ref("email").withSchema(TableName.Users).as("createdByEmail")) + .first(); + return doc; + } catch (err) { + throw new DatabaseError({ error: err, name: "FindById" }); + } + }; + + return { ...stOrm, findById }; }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 76d33bd6b4..cce0d3780f 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -142,7 +142,7 @@ export const serviceTokenServiceFactory = ({ const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { lastUsed: new Date() }); - return updatedToken; + return { ...serviceToken, lastUsed: updatedToken.lastUsed }; }; return { diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 142993ceb9..7ebeaa227a 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/organizationInvitation.handlebars b/backend/src/services/smtp/templates/organizationInvitation.handlebars index b281786f4e..024fca1321 100644 --- a/backend/src/services/smtp/templates/organizationInvitation.handlebars +++ b/backend/src/services/smtp/templates/organizationInvitation.handlebars @@ -8,7 +8,7 @@

Join your organization on Infisical

-

{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization β€” {{organizationName}}

+

{{inviterFirstName}} ({{inviterUsername}}) has invited you to their 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.

diff --git a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars new file mode 100644 index 0000000000..b1482aa172 --- /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 60556555cd..39a9b74ba5 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 1144bd414a..d76ad3ec37 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,4 +1,5 @@ import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; @@ -14,6 +15,7 @@ type TSuperAdminServiceFactoryDep = { userDAL: TUserDALFactory; authService: Pick; orgService: Pick; + keyStore: Pick; }; export type TSuperAdminServiceFactory = ReturnType; @@ -21,26 +23,53 @@ export type TSuperAdminServiceFactory = ReturnType Promise; +const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; +const ADMIN_CONFIG_KEY_EXP = 60; // 60s +const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; + export const superAdminServiceFactory = ({ serverCfgDAL, userDAL, authService, - orgService + orgService, + keyStore }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself - getServerCfg = () => serverCfgDAL.findOne({}); + getServerCfg = async () => { + const config = await keyStore.getItem(ADMIN_CONFIG_KEY); + // missing in keystore means fetch from db + if (!config) { + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfg) { + await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore + } + return serverCfg; + } - const serverCfg = await serverCfgDAL.findOne({}); + const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin; + return { + ...keyStoreServerCfg, + // this is to allow admin router to work + createdAt: new Date(keyStoreServerCfg.createdAt), + updatedAt: new Date(keyStoreServerCfg.updatedAt) + }; + }; + + // reset on initialized + await keyStore.deleteItem(ADMIN_CONFIG_KEY); + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); if (serverCfg) return; - const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true }); + + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true, id: ADMIN_CONFIG_DB_UUID }); return newCfg; }; const updateServerCfg = async (data: TSuperAdminUpdate) => { - const serverCfg = await getServerCfg(); - const cfg = await serverCfgDAL.updateById(serverCfg.id, data); - return cfg; + const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data); + await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); + return updatedServerCfg; }; const adminSignUp = async ({ @@ -68,8 +97,10 @@ export const superAdminServiceFactory = ({ { firstName, lastName, + username: email, email, superAdmin: true, + isGhost: false, isAccepted: true, authMethods: [AuthMethod.EMAIL] }, @@ -96,7 +127,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({ + userId: userInfo.user.id, + userEmail: userInfo.user.email, + orgName: initialOrganizationName + }); await updateServerCfg({ initialized: true }); const token = await authService.generateUserTokens({ @@ -106,7 +141,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/telemetry/telemetry-dal.ts b/backend/src/services/telemetry/telemetry-dal.ts new file mode 100644 index 0000000000..9fac4f1ef9 --- /dev/null +++ b/backend/src/services/telemetry/telemetry-dal.ts @@ -0,0 +1,39 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; + +export type TTelemetryDALFactory = ReturnType; + +export const telemetryDALFactory = (db: TDbClient) => { + const getTelemetryInstanceStats = async () => { + try { + const userCount = (await db(TableName.Users).where({ isGhost: false }).count().first())?.count as string; + const users = parseInt(userCount || "0", 10); + + const identityCount = (await db(TableName.Identity).count().first())?.count as string; + const identities = parseInt(identityCount || "0", 10); + + const projectCount = (await db(TableName.Project).count().first())?.count as string; + const projects = parseInt(projectCount || "0", 10); + + const secretCount = (await db(TableName.Secret).count().first())?.count as string; + const secrets = parseInt(secretCount || "0", 10); + + const organizationNames = await db(TableName.Organization).select("name"); + const organizations = organizationNames.length; + + return { + users, + identities, + projects, + secrets, + organizations, + organizationNames: organizationNames.map(({ name }) => name) + }; + } catch (error) { + throw new DatabaseError({ error, name: "TelemtryInstanceStats" }); + } + }; + + return { getTelemetryInstanceStats }; +}; diff --git a/backend/src/services/telemetry/telemetry-queue.ts b/backend/src/services/telemetry/telemetry-queue.ts new file mode 100644 index 0000000000..02e906fe69 --- /dev/null +++ b/backend/src/services/telemetry/telemetry-queue.ts @@ -0,0 +1,78 @@ +import { PostHog } from "posthog-node"; + +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { getServerCfg } from "../super-admin/super-admin-service"; +import { TTelemetryDALFactory } from "./telemetry-dal"; +import { TELEMETRY_SECRET_OPERATIONS_KEY, TELEMETRY_SECRET_PROCESSED_KEY } from "./telemetry-service"; +import { PostHogEventTypes } from "./telemetry-types"; + +type TTelemetryQueueServiceFactoryDep = { + queueService: TQueueServiceFactory; + keyStore: Pick; + telemetryDAL: TTelemetryDALFactory; +}; + +export type TTelemetryQueueServiceFactory = ReturnType; + +export const telemetryQueueServiceFactory = ({ + queueService, + keyStore, + telemetryDAL +}: TTelemetryQueueServiceFactoryDep) => { + const appCfg = getConfig(); + const postHog = + appCfg.isProductionMode && appCfg.TELEMETRY_ENABLED + ? new PostHog(appCfg.POSTHOG_PROJECT_API_KEY, { host: appCfg.POSTHOG_HOST, flushAt: 1, flushInterval: 0 }) + : undefined; + + queueService.start(QueueName.TelemetryInstanceStats, async () => { + const { instanceId } = await getServerCfg(); + const telemtryStats = await telemetryDAL.getTelemetryInstanceStats(); + // parse the redis values into integer + const numberOfSecretOperationsMade = parseInt((await keyStore.getItem(TELEMETRY_SECRET_OPERATIONS_KEY)) || "0", 10); + const numberOfSecretProcessed = parseInt((await keyStore.getItem(TELEMETRY_SECRET_PROCESSED_KEY)) || "0", 10); + const stats = { ...telemtryStats, numberOfSecretProcessed, numberOfSecretOperationsMade }; + + // send to postHog + postHog?.capture({ + event: PostHogEventTypes.TelemetryInstanceStats, + distinctId: instanceId, + properties: stats + }); + // reset the stats + await keyStore.deleteItem(TELEMETRY_SECRET_PROCESSED_KEY); + await keyStore.deleteItem(TELEMETRY_SECRET_OPERATIONS_KEY); + }); + + // every day at midnight a telemetry job executes on self hosted + // this sends some telemetry information like instance id secrets operated etc + const startTelemetryCheck = async () => { + // this is a fast way to check its cloud or not + if (appCfg.INFISICAL_CLOUD) return; + // clear previous job + await queueService.stopRepeatableJob( + QueueName.TelemetryInstanceStats, + QueueJobs.TelemetryInstanceStats, + { pattern: "0 0 * * *", utc: true }, + QueueName.TelemetryInstanceStats // just a job id + ); + if (postHog) { + await queueService.queue(QueueName.TelemetryInstanceStats, QueueJobs.TelemetryInstanceStats, undefined, { + jobId: QueueName.TelemetryInstanceStats, + repeat: { pattern: "0 0 * * *", utc: true } + }); + } + }; + + queueService.listen(QueueName.TelemetryInstanceStats, "failed", (err) => { + logger.error(err?.failedReason, `${QueueName.TelemetryInstanceStats}: failed`); + }); + + return { + startTelemetryCheck + }; +}; diff --git a/backend/src/services/telemetry/telemetry-service.ts b/backend/src/services/telemetry/telemetry-service.ts index c386abd955..9912e0101c 100644 --- a/backend/src/services/telemetry/telemetry-service.ts +++ b/backend/src/services/telemetry/telemetry-service.ts @@ -1,15 +1,24 @@ import { PostHog } from "posthog-node"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { InstanceType } from "@app/ee/services/license/license-types"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; -import { TPostHogEvent } from "./telemetry-types"; +import { PostHogEventTypes, TPostHogEvent, TSecretModifiedEvent } from "./telemetry-types"; + +export const TELEMETRY_SECRET_PROCESSED_KEY = "telemetry-secret-processed"; +export const TELEMETRY_SECRET_OPERATIONS_KEY = "telemetry-secret-operations"; export type TTelemetryServiceFactory = ReturnType; +export type TTelemetryServiceFactoryDep = { + keyStore: Pick; + licenseService: Pick; +}; -// type TTelemetryServiceFactoryDep = {}; -export const telemetryServiceFactory = () => { +export const telemetryServiceFactory = ({ keyStore, licenseService }: TTelemetryServiceFactoryDep) => { const appCfg = getConfig(); if (appCfg.isProductionMode && !appCfg.TELEMETRY_ENABLED) { @@ -21,10 +30,9 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme `); } - const postHog = - appCfg.isProductionMode && appCfg.TELEMETRY_ENABLED - ? new PostHog(appCfg.POSTHOG_PROJECT_API_KEY, { host: appCfg.POSTHOG_HOST }) - : undefined; + const postHog = appCfg.TELEMETRY_ENABLED + ? new PostHog(appCfg.POSTHOG_PROJECT_API_KEY, { host: appCfg.POSTHOG_HOST }) + : undefined; // used for email marketting email sending purpose const sendLoopsEvent = async (email: string, firstName?: string, lastName?: string) => { @@ -51,18 +59,45 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme } }; - const sendPostHogEvents = (event: TPostHogEvent) => { + const sendPostHogEvents = async (event: TPostHogEvent) => { if (postHog) { - postHog.capture({ - event: event.event, - distinctId: event.distinctId, - properties: event.properties - }); + const instanceType = licenseService.getInstanceType(); + // capture posthog only when its cloud or signup event happens in self hosted + if (instanceType === InstanceType.Cloud || event.event === PostHogEventTypes.UserSignedUp) { + postHog.capture({ + event: event.event, + distinctId: event.distinctId, + properties: event.properties + }); + return; + } + + if ( + [ + PostHogEventTypes.SecretPulled, + PostHogEventTypes.SecretCreated, + PostHogEventTypes.SecretDeleted, + PostHogEventTypes.SecretUpdated + ].includes(event.event) + ) { + await keyStore.incrementBy( + TELEMETRY_SECRET_PROCESSED_KEY, + (event as TSecretModifiedEvent).properties.numberOfSecrets + ); + await keyStore.incrementBy(TELEMETRY_SECRET_OPERATIONS_KEY, 1); + } + } + }; + + const flushAll = async () => { + if (postHog) { + await postHog.shutdownAsync(); } }; return { sendLoopsEvent, - sendPostHogEvents + sendPostHogEvents, + flushAll }; }; diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index d97c3c7acc..b168fe5d73 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -8,7 +8,12 @@ export enum PostHogEventTypes { UserSignedUp = "User Signed Up", SecretRotated = "secrets rotated", SecretScannerFull = "historical cloud secret scan", - SecretScannerPush = "cloud secret scan" + SecretScannerPush = "cloud secret scan", + ProjectCreated = "Project Created", + IntegrationCreated = "Integration Created", + MachineIdentityCreated = "Machine Identity Created", + UserOrgInvitation = "User Org Invitation", + TelemetryInstanceStats = "Self Hosted Instance Stats" } export type TSecretModifiedEvent = { @@ -32,6 +37,7 @@ export type TSecretModifiedEvent = { export type TAdminInitEvent = { event: PostHogEventTypes.AdminInit; properties: { + username: string; email: string; firstName: string; lastName: string; @@ -41,6 +47,7 @@ export type TAdminInitEvent = { export type TUserSignedUpEvent = { event: PostHogEventTypes.UserSignedUp; properties: { + username: string; email: string; attributionSource?: string; }; @@ -53,9 +60,72 @@ export type TSecretScannerEvent = { }; }; +export type TProjectCreateEvent = { + event: PostHogEventTypes.ProjectCreated; + properties: { + name: string; + orgId: string; + }; +}; + +export type TMachineIdentityCreatedEvent = { + event: PostHogEventTypes.MachineIdentityCreated; + properties: { + name: string; + orgId: string; + identityId: string; + }; +}; + +export type TIntegrationCreatedEvent = { + event: PostHogEventTypes.IntegrationCreated; + properties: { + projectId: string; + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + url?: string; + app?: string; + appId?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + targetService?: string; + targetServiceId?: string; + path?: string; + region?: string; + }; +}; + +export type TUserOrgInvitedEvent = { + event: PostHogEventTypes.UserOrgInvitation; + properties: { + inviteeEmail: string; + }; +}; + +export type TTelemetryInstanceStatsEvent = { + event: PostHogEventTypes.TelemetryInstanceStats; + properties: { + users: number; + identities: number; + projects: number; + secrets: number; + organizations: number; + organizationNames: number; + numberOfSecretOperationsMade: number; + numberOfSecretProcessed: number; + }; +}; + export type TPostHogEvent = { distinctId: string } & ( | TSecretModifiedEvent | TAdminInitEvent | TUserSignedUpEvent | TSecretScannerEvent + | TUserOrgInvitedEvent + | TMachineIdentityCreatedEvent + | TIntegrationCreatedEvent + | TProjectCreateEvent + | TTelemetryInstanceStatsEvent ); diff --git a/backend/src/services/user-alias/user-alias-dal.ts b/backend/src/services/user-alias/user-alias-dal.ts new file mode 100644 index 0000000000..366eadce0e --- /dev/null +++ b/backend/src/services/user-alias/user-alias-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TUserAliasDALFactory = ReturnType; + +export const userAliasDALFactory = (db: TDbClient) => { + const userAliasOrm = ormify(db, TableName.UserAliases); + + return { + ...userAliasOrm + }; +}; diff --git a/backend/src/services/user-alias/user-alias-types.ts b/backend/src/services/user-alias/user-alias-types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 0de4903991..425e8215c6 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -16,14 +16,17 @@ export type TUserDALFactory = ReturnType; export const userDALFactory = (db: TDbClient) => { const userOrm = ormify(db, TableName.Users); - const findUserByEmail = async (email: string, tx?: Knex) => userOrm.findOne({ email }, tx); + const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx); // USER ENCRYPTION FUNCTIONS // ------------------------- - const findUserEncKeyByEmail = async (email: string) => { + const findUserEncKeyByUsername = async ({ username }: { username: string }) => { try { return await db(TableName.Users) - .where({ email }) + .where({ + username, + isGhost: false + }) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .first(); } catch (error) { @@ -47,6 +50,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("*"); @@ -107,10 +121,11 @@ export const userDALFactory = (db: TDbClient) => { return { ...userOrm, - findUserByEmail, - findUserEncKeyByEmail, + findUserByUsername, + findUserEncKeyByUsername, findUserEncKeyByUserId, updateUserEncryptionByUserId, + findUserByProjectMembershipId, upsertUserEncryptionKey, createUserEncryption, findOneUserAction, diff --git a/backend/src/services/user/user-fns.ts b/backend/src/services/user/user-fns.ts new file mode 100644 index 0000000000..23789df1bd --- /dev/null +++ b/backend/src/services/user/user-fns.ts @@ -0,0 +1,21 @@ +import slugify from "@sindresorhus/slugify"; + +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { TUserDALFactory } from "@app/services/user/user-dal"; + +export const normalizeUsername = async (username: string, userDAL: Pick) => { + let attempt = slugify(username); + + let user = await userDAL.findOne({ username: attempt }); + if (!user) return attempt; + + while (true) { + attempt = slugify(`${username}-${alphaNumericNanoId(4)}`); + // eslint-disable-next-line no-await-in-loop + user = await userDAL.findOne({ username: attempt }); + + if (!user) { + return attempt; + } + } +}; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index eebfd958f1..c85e40eb3c 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -11,6 +11,10 @@ export type TUserServiceFactory = ReturnType; export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => { const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => { + const user = await userDAL.findById(userId); + + if (!user || !user.email) throw new BadRequestError({ name: "Failed to toggle MFA" }); + const updatedUser = await userDAL.updateById(userId, { isMfaEnabled, mfaMethods: isMfaEnabled ? ["email"] : [] @@ -30,6 +34,12 @@ export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => { const user = await userDAL.findById(userId); if (!user) throw new BadRequestError({ name: "Update auth methods" }); + if (user.authMethods?.includes(AuthMethod.LDAP)) + throw new BadRequestError({ message: "LDAP auth method cannot be updated", name: "Update auth methods" }); + + if (authMethods.includes(AuthMethod.LDAP)) + throw new BadRequestError({ message: "LDAP auth method cannot be updated", name: "Update auth methods" }); + const updatedUser = await userDAL.updateById(userId, { authMethods }); return updatedUser; }; diff --git a/backend/tsup.config.js b/backend/tsup.config.js index 4e182b9d2c..9e37870ba5 100644 --- a/backend/tsup.config.js +++ b/backend/tsup.config.js @@ -23,7 +23,8 @@ export default defineConfig({ loader: { ".handlebars": "copy", ".md": "copy", - ".txt": "copy" + ".txt": "copy", + ".pem": "copy" }, external: ["../../../frontend/node_modules/next/dist/server/next-server.js"], outDir: "dist", diff --git a/backend/vitest.e2e.config.ts b/backend/vitest.e2e.config.ts index e8636d4051..c660fed14e 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/cli/agent-config.yaml b/cli/agent-config.yaml index ae130d3a8e..e767fdca52 100644 --- a/cli/agent-config.yaml +++ b/cli/agent-config.yaml @@ -1,5 +1,5 @@ infisical: - address: "http://localhost:8080" + address: "https://app.infisical.com/" auth: type: "universal-auth" config: @@ -13,3 +13,12 @@ sinks: templates: - source-path: my-dot-ev-secret-template destination-path: my-dot-env.env + config: + polling-interval: 60s + execute: + command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d + - source-path: my-dot-ev-secret-template1 + destination-path: my-dot-env-1.env + config: + exec: + command: mkdir hello-world1 diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 9ae356cd0a..1b69b8f23d 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -145,6 +145,25 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo return loginTwoV2Response, nil } +func CallGetAllOrganizations(httpClient *resty.Client) (GetOrganizationsResponse, error) { + var orgResponse GetOrganizationsResponse + response, err := httpClient. + R(). + SetResult(&orgResponse). + SetHeader("User-Agent", USER_AGENT). + Get(fmt.Sprintf("%v/v1/organization", config.INFISICAL_URL)) + + if err != nil { + return GetOrganizationsResponse{}, err + } + + if response.IsError() { + return GetOrganizationsResponse{}, fmt.Errorf("CallGetAllOrganizations: Unsuccessful response: [response=%v]", response) + } + + return orgResponse, nil +} + func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) { var workSpacesResponse GetWorkSpacesResponse response, err := httpClient. @@ -490,5 +509,7 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) } + getRawSecretsV3Response.ETag = response.Header().Get(("etag")) + return getRawSecretsV3Response, nil } diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 3c6466382e..3109760787 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -120,14 +120,21 @@ type PullSecretsByInfisicalTokenResponse struct { type GetWorkSpacesResponse struct { Workspaces []struct { - ID string `json:"_id"` - Name string `json:"name"` - Plan string `json:"plan,omitempty"` - V int `json:"__v"` - Organization string `json:"organization,omitempty"` + ID string `json:"_id"` + Name string `json:"name"` + Plan string `json:"plan,omitempty"` + V int `json:"__v"` + OrganizationId string `json:"orgId"` } `json:"workspaces"` } +type GetOrganizationsResponse struct { + Organizations []struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"organizations"` +} + type Secret struct { SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"` SecretKeyIV string `json:"secretKeyIV,omitempty"` @@ -292,10 +299,10 @@ type GetFoldersV1Response struct { } type CreateFolderV1Request struct { - FolderName string `json:"folderName"` + FolderName string `json:"name"` WorkspaceId string `json:"workspaceId"` Environment string `json:"environment"` - Directory string `json:"directory"` + Path string `json:"path"` } type CreateFolderV1Response struct { @@ -505,4 +512,5 @@ type GetRawSecretsV3Response struct { SecretComment string `json:"secretComment"` } `json:"secrets"` Imports []any `json:"imports"` + ETag string } diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index 8857f58061..db7c812254 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -5,12 +5,15 @@ package cmd import ( "bytes" + "context" "encoding/base64" "fmt" "io/ioutil" "os" + "os/exec" "os/signal" "path" + "runtime" "strings" "sync" "syscall" @@ -71,12 +74,56 @@ type Template struct { SourcePath string `yaml:"source-path"` Base64TemplateContent string `yaml:"base64-template-content"` DestinationPath string `yaml:"destination-path"` + + Config struct { // Configurations for the template + PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret + Execute struct { + Command string `yaml:"command"` // Command to execute once the template has been rendered + Timeout int64 `yaml:"timeout"` // Timeout for the command + } `yaml:"execute"` // Command to execute once the template has been rendered + } `yaml:"config"` } func ReadFile(filePath string) ([]byte, error) { return ioutil.ReadFile(filePath) } +func ExecuteCommandWithTimeout(command string, timeout int64) error { + + shell := [2]string{"sh", "-c"} + if runtime.GOOS == "windows" { + shell = [2]string{"cmd", "/C"} + } else { + currentShell := os.Getenv("SHELL") + if currentShell != "" { + shell[0] = currentShell + } + } + + ctx := context.Background() + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + } + + cmd := exec.CommandContext(ctx, shell[0], shell[1], command) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + if exitError, ok := err.(*exec.ExitError); ok { // type assertion + if exitError.ProcessState.ExitCode() == -1 { + return fmt.Errorf("command timed out") + } + } + return err + } else { + return nil + } +} + func FileExists(filepath string) bool { info, err := os.Stat(filepath) if os.IsNotExist(err) { @@ -170,20 +217,26 @@ func ParseAgentConfig(configFile []byte) (*Config, error) { return config, nil } -func secretTemplateFunction(accessToken string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { +func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) { - secrets, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false) + res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false) if err != nil { return nil, err } - return secrets, nil + if existingEtag != res.Etag { + *currentEtag = res.Etag + } + + expandedSecrets := util.ExpandSecrets(res.Secrets, models.ExpandSecretsAuthentication{UniversalAuthAccessToken: accessToken}, "") + + return expandedSecrets, nil } } -func ProcessTemplate(templatePath string, data interface{}, accessToken string) (*bytes.Buffer, error) { +func ProcessTemplate(templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) { // custom template function to fetch secrets from Infisical - secretFunction := secretTemplateFunction(accessToken) + secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) funcs := template.FuncMap{ "secret": secretFunction, } @@ -203,7 +256,7 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string) return &buf, nil } -func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken string) (*bytes.Buffer, error) { +func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string) (*bytes.Buffer, error) { // custom template function to fetch secrets from Infisical decoded, err := base64.StdEncoding.DecodeString(encodedTemplate) if err != nil { @@ -212,7 +265,7 @@ func ProcessBase64Template(encodedTemplate string, data interface{}, accessToken templateString := string(decoded) - secretFunction := secretTemplateFunction(accessToken) + secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this funcs := template.FuncMap{ "secret": secretFunction, } @@ -250,7 +303,16 @@ type TokenManager struct { } func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool, exitAfterAuth bool) *TokenManager { - return &TokenManager{filePaths: fileDeposits, templates: templates, clientIdPath: clientIdPath, clientSecretPath: clientSecretPath, newAccessTokenNotificationChan: newAccessTokenNotificationChan, removeClientSecretOnRead: removeClientSecretOnRead, exitAfterAuth: exitAfterAuth} + return &TokenManager{ + filePaths: fileDeposits, + templates: templates, + clientIdPath: clientIdPath, + clientSecretPath: clientSecretPath, + newAccessTokenNotificationChan: newAccessTokenNotificationChan, + removeClientSecretOnRead: removeClientSecretOnRead, + exitAfterAuth: exitAfterAuth, + } + } func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) { @@ -428,38 +490,80 @@ func (tm *TokenManager) WriteTokenToFiles() { } } -func (tm *TokenManager) FetchSecrets() { - log.Info().Msgf("template engine started...") +func (tm *TokenManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) { + if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil { + log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err) + return + } + log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", template.SourcePath, template.DestinationPath) +} + +func (tm *TokenManager) MonitorSecretChanges(secretTemplate Template, sigChan chan os.Signal) { + + pollingInterval := time.Duration(5 * time.Minute) + + if secretTemplate.Config.PollingInterval != "" { + interval, err := util.ConvertPollingIntervalToTime(secretTemplate.Config.PollingInterval) + + if err != nil { + log.Error().Msgf("unable to convert polling interval to time because %v", err) + sigChan <- syscall.SIGINT + return + + } else { + pollingInterval = interval + } + } + + var existingEtag string + var currentEtag string + var firstRun = true + + execTimeout := secretTemplate.Config.Execute.Timeout + execCommand := secretTemplate.Config.Execute.Command + for { token := tm.GetToken() + if token != "" { - for _, secretTemplate := range tm.templates { - var processedTemplate *bytes.Buffer - var err error - if secretTemplate.SourcePath != "" { - processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token) - } else { - processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token) - } - if err != nil { - log.Error().Msgf("template engine: unable to render secrets because %s. Will try again on next cycle", err) + var processedTemplate *bytes.Buffer + var err error - continue - } - - if err := WriteBytesToFile(processedTemplate, secretTemplate.DestinationPath); err != nil { - log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err) - - continue - } - - log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", secretTemplate.SourcePath, secretTemplate.DestinationPath) + if secretTemplate.SourcePath != "" { + processedTemplate, err = ProcessTemplate(secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag) + } else { + processedTemplate, err = ProcessBase64Template(secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag) } - // fetch new secrets every 5 minutes (TODO: add PubSub in the future ) - time.Sleep(5 * time.Minute) + if err != nil { + log.Error().Msgf("unable to process template because %v", err) + } else { + if (existingEtag != currentEtag) || firstRun { + + tm.WriteTemplateToFile(processedTemplate, &secretTemplate) + existingEtag = currentEtag + + if !firstRun && execCommand != "" { + log.Info().Msgf("executing command: %s", execCommand) + err := ExecuteCommandWithTimeout(execCommand, execTimeout) + + if err != nil { + log.Error().Msgf("unable to execute command because %v", err) + } + + } + if firstRun { + firstRun = false + } + } + } + time.Sleep(pollingInterval) + } else { + // It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render. + time.Sleep(3 * time.Second) } + } } @@ -520,7 +624,7 @@ var agentCmd = &cobra.Command{ } if !FileExists(configPath) && agentConfigInBase64 == "" { - log.Error().Msgf("No agent config file provided. Please provide a agent config file", configPath) + log.Error().Msgf("No agent config file provided at %v. Please provide a agent config file", configPath) return } @@ -544,7 +648,11 @@ var agentCmd = &cobra.Command{ tm := NewTokenManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead, agentConfig.Infisical.ExitAfterAuth) go tm.ManageTokenLifecycle() - go tm.FetchSecrets() + + for i, template := range agentConfig.Templates { + log.Info().Msgf("template engine started for template %v...", i+1) + go tm.MonitorSecretChanges(template, sigChan) + } for { select { diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index d0db485b38..1a34219ebf 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -59,7 +59,8 @@ var exportCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - infisicalToken, err := cmd.Flags().GetString("token") + infisicalToken, err := util.GetInfisicalServiceToken(cmd) + if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -87,16 +88,14 @@ var exportCmd = &cobra.Command{ var output string if shouldExpandSecrets { - substitutions := util.ExpandSecrets(secrets, infisicalToken, "") - output, err = formatEnvs(substitutions, format) - if err != nil { - util.HandleError(err) - } - } else { - output, err = formatEnvs(secrets, format) - if err != nil { - util.HandleError(err) - } + secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{ + InfisicalToken: infisicalToken, + }, "") + } + secrets = util.FilterSecretsByTag(secrets, tagSlugs) + output, err = formatEnvs(secrets, format) + if err != nil { + util.HandleError(err) } fmt.Print(output) diff --git a/cli/packages/cmd/folder.go b/cli/packages/cmd/folder.go index 12e2206fa2..290f32c380 100644 --- a/cli/packages/cmd/folder.go +++ b/cli/packages/cmd/folder.go @@ -36,7 +36,8 @@ var getCmd = &cobra.Command{ } } - infisicalToken, err := cmd.Flags().GetString("token") + infisicalToken, err := util.GetInfisicalServiceToken(cmd) + if err != nil { util.HandleError(err, "Unable to parse flag") } diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index 070074fa94..e47eb447e2 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -5,7 +5,6 @@ package cmd import ( "encoding/json" - "fmt" "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/models" @@ -52,25 +51,19 @@ var initCmd = &cobra.Command{ httpClient := resty.New() httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken) - workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient) + + organizationResponse, err := api.CallGetAllOrganizations(httpClient) if err != nil { - util.HandleError(err, "Unable to pull projects that belong to you") + util.HandleError(err, "Unable to pull organizations that belong to you") } - workspaces := workspaceResponse.Workspaces - if len(workspaces) == 0 { - message := fmt.Sprintf("You don't have any projects created in Infisical. You must first create a project at %s", util.INFISICAL_TOKEN_NAME) - util.PrintErrorMessageAndExit(message) - } + organizations := organizationResponse.Organizations - var workspaceNames []string - for _, workspace := range workspaces { - workspaceNames = append(workspaceNames, workspace.Name) - } + organizationNames := util.GetOrganizationsNameList(organizationResponse) prompt := promptui.Select{ - Label: "Which of your Infisical projects would you like to connect this project to?", - Items: workspaceNames, + Label: "Which Infisical organization would you like to select a project from?", + Items: organizationNames, Size: 7, } @@ -79,7 +72,27 @@ var initCmd = &cobra.Command{ util.HandleError(err) } - err = writeWorkspaceFile(workspaces[index]) + selectedOrganization := organizations[index] + + workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient) + if err != nil { + util.HandleError(err, "Unable to pull projects that belong to you") + } + + filteredWorkspaces, workspaceNames := util.GetWorkspacesInOrganization(workspaceResponse, selectedOrganization.ID) + + prompt = promptui.Select{ + Label: "Which of your Infisical projects would you like to connect this project to?", + Items: workspaceNames, + Size: 7, + } + + index, _, err = prompt.Run() + if err != nil { + util.HandleError(err) + } + + err = writeWorkspaceFile(filteredWorkspaces[index]) if err != nil { util.HandleError(err) } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 2bb043c263..d008af1ecc 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -62,7 +62,8 @@ var runCmd = &cobra.Command{ } } - infisicalToken, err := cmd.Flags().GetString("token") + infisicalToken, err := util.GetInfisicalServiceToken(cmd) + if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -110,7 +111,9 @@ var runCmd = &cobra.Command{ } if shouldExpandSecrets { - secrets = util.ExpandSecrets(secrets, infisicalToken, projectConfigDir) + secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{ + InfisicalToken: infisicalToken, + }, projectConfigDir) } secretsByKey := getSecretsByKeys(secrets) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 778ac574e1..ffe82179c5 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -38,7 +38,12 @@ var secretsCmd = &cobra.Command{ } } - infisicalToken, err := cmd.Flags().GetString("token") + infisicalToken, err := util.GetInfisicalServiceToken(cmd) + + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -80,7 +85,9 @@ var secretsCmd = &cobra.Command{ } if shouldExpandSecrets { - secrets = util.ExpandSecrets(secrets, infisicalToken, "") + secrets = util.ExpandSecrets(secrets, models.ExpandSecretsAuthentication{ + InfisicalToken: infisicalToken, + }, "") } visualize.PrintAllSecretDetails(secrets) @@ -391,7 +398,8 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } } - infisicalToken, err := cmd.Flags().GetString("token") + infisicalToken, err := util.GetInfisicalServiceToken(cmd) + if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -406,6 +414,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse path flag") } + showOnlyValue, err := cmd.Flags().GetBool("raw-value") + if err != nil { + util.HandleError(err, "Unable to parse path flag") + } + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}, "") if err != nil { util.HandleError(err, "To fetch all secrets") @@ -427,7 +440,15 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } } - visualize.PrintAllSecretDetails(requestedSecrets) + if showOnlyValue && len(requestedSecrets) > 1 { + util.PrintErrorMessageAndExit("--raw-value only works with one secret.") + } + + if showOnlyValue { + fmt.Printf(requestedSecrets[0].Value) + } else { + visualize.PrintAllSecretDetails(requestedSecrets) + } Telemetry.CaptureEvent("cli-command:secrets get", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) } @@ -445,7 +466,8 @@ func generateExampleEnv(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } - infisicalToken, err := cmd.Flags().GetString("token") + infisicalToken, err := util.GetInfisicalServiceToken(cmd) + if err != nil { util.HandleError(err, "Unable to parse flag") } @@ -661,6 +683,7 @@ func init() { secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsCmd.AddCommand(secretsGetCmd) secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") + secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 165982a77e..576e749092 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -21,11 +21,12 @@ type LoggedInUser struct { } type SingleEnvironmentVariable struct { - Key string `json:"key"` - Value string `json:"value"` - Type string `json:"type"` - ID string `json:"_id"` - Tags []struct { + Key string `json:"key"` + WorkspaceId string `json:"workspace"` + Value string `json:"value"` + Type string `json:"type"` + ID string `json:"_id"` + Tags []struct { ID string `json:"_id"` Name string `json:"name"` Slug string `json:"slug"` @@ -34,17 +35,22 @@ type SingleEnvironmentVariable struct { Comment string `json:"comment"` } +type PlaintextSecretResult struct { + Secrets []SingleEnvironmentVariable + Etag string +} + type SingleFolder struct { ID string `json:"_id"` Name string `json:"name"` } type Workspace struct { - ID string `json:"_id"` - Name string `json:"name"` - Plan string `json:"plan,omitempty"` - V int `json:"__v"` - Organization string `json:"organization,omitempty"` + ID string `json:"_id"` + Name string `json:"name"` + Plan string `json:"plan,omitempty"` + V int `json:"__v"` + OrganizationId string `json:"orgId"` } type WorkspaceConfigFile struct { @@ -63,6 +69,7 @@ type GetAllSecretsParameters struct { Environment string EnvironmentPassedViaFlag bool InfisicalToken string + UniversalAuthAccessToken string TagSlugs string WorkspaceId string SecretsPath string @@ -91,3 +98,8 @@ type DeleteFolderParameters struct { FolderPath string InfisicalToken string } + +type ExpandSecretsAuthentication struct { + InfisicalToken string + UniversalAuthAccessToken string +} diff --git a/cli/packages/util/agent.go b/cli/packages/util/agent.go new file mode 100644 index 0000000000..188ae5de25 --- /dev/null +++ b/cli/packages/util/agent.go @@ -0,0 +1,41 @@ +package util + +import ( + "fmt" + "strconv" + "time" +) + +// ConvertPollingIntervalToTime converts a string representation of a polling interval to a time.Duration +func ConvertPollingIntervalToTime(pollingInterval string) (time.Duration, error) { + length := len(pollingInterval) + if length < 2 { + return 0, fmt.Errorf("invalid format") + } + + unit := pollingInterval[length-1:] + numberPart := pollingInterval[:length-1] + + number, err := strconv.Atoi(numberPart) + if err != nil { + return 0, err + } + + switch unit { + case "s": + if number < 60 { + return 0, fmt.Errorf("polling interval should be at least 60 seconds") + } + return time.Duration(number) * time.Second, nil + case "m": + return time.Duration(number) * time.Minute, nil + case "h": + return time.Duration(number) * time.Hour, nil + case "d": + return time.Duration(number) * 24 * time.Hour, nil + case "w": + return time.Duration(number) * 7 * 24 * time.Hour, nil + default: + return 0, fmt.Errorf("invalid time unit") + } +} diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index 7275653d9b..0f837ee71a 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -154,7 +154,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er WorkspaceId: params.WorkspaceId, Environment: params.Environment, FolderName: params.FolderName, - Directory: params.FolderPath, + Path: params.FolderPath, } apiResponse, err := api.CallCreateFolderV1(httpClient, createFolderRequest) diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 043db468e0..11b3396dc2 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/Infisical/infisical-merge/packages/models" + "github.com/spf13/cobra" ) type DecodedSymmetricEncryptionDetails = struct { @@ -63,6 +64,20 @@ func IsSecretTypeValid(s string) bool { return false } +func GetInfisicalServiceToken(cmd *cobra.Command) (serviceToken string, err error) { + infisicalToken, err := cmd.Flags().GetString("token") + + if infisicalToken == "" { + infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) + } + + if err != nil { + return "", err + } + + return infisicalToken, nil +} + // Checks if the passed in email already exists in the users slice func ConfigContainsEmail(users []models.LoggedInUser, email string) bool { for _, value := range users { @@ -82,6 +97,11 @@ func RequireLogin() { } } +func IsLoggedIn() bool { + configFile, _ := GetConfigFile() + return configFile.LoggedInUserEmail != "" +} + func RequireServiceToken() { serviceToken := os.Getenv(INFISICAL_TOKEN_NAME) if serviceToken == "" { diff --git a/cli/packages/util/init.go b/cli/packages/util/init.go new file mode 100644 index 0000000000..33350f3b77 --- /dev/null +++ b/cli/packages/util/init.go @@ -0,0 +1,45 @@ +package util + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/models" +) + +func GetOrganizationsNameList(organizationResponse api.GetOrganizationsResponse) []string { + organizations := organizationResponse.Organizations + + if len(organizations) == 0 { + message := fmt.Sprintf("You don't have any organization created in Infisical. You must first create a organization at %s", INFISICAL_DEFAULT_URL) + PrintErrorMessageAndExit(message) + } + + var organizationNames []string + for _, workspace := range organizations { + organizationNames = append(organizationNames, workspace.Name) + } + + return organizationNames +} + +func GetWorkspacesInOrganization(workspaceResponse api.GetWorkSpacesResponse, orgId string) ([]models.Workspace, []string) { + workspaces := workspaceResponse.Workspaces + + var filteredWorkspaces []models.Workspace + var workspaceNames []string + + for _, workspace := range workspaces { + if workspace.OrganizationId == orgId { + filteredWorkspaces = append(filteredWorkspaces, workspace) + workspaceNames = append(workspaceNames, workspace.Name) + } + } + + if len(filteredWorkspaces) == 0 { + message := fmt.Sprintf("You don't have any projects created in Infisical organization. You must first create a project at %s", INFISICAL_DEFAULT_URL) + PrintErrorMessageAndExit(message) + } + + return filteredWorkspaces, workspaceNames +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index a260aaaec4..8c142a4ecd 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -152,7 +152,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work return plainTextSecrets, nil } -func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) (models.PlaintextSecretResult, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -170,16 +170,16 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin rawSecrets, err := api.CallGetRawSecretsV3(httpClient, api.GetRawSecretsV3Request{WorkspaceId: workspaceId, SecretPath: secretsPath, Environment: environmentName}) if err != nil { - return nil, err + return models.PlaintextSecretResult{}, err } plainTextSecrets := []models.SingleEnvironmentVariable{} if err != nil { - return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) + return models.PlaintextSecretResult{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) } for _, secret := range rawSecrets.Secrets { - plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue}) + plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, WorkspaceId: secret.Workspace}) } // if includeImports { @@ -189,7 +189,10 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin // } // } - return plainTextSecrets, nil + return models.PlaintextSecretResult{ + Secrets: plainTextSecrets, + Etag: rawSecrets.ETag, + }, nil } func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) { @@ -220,12 +223,33 @@ func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleE return secrets, nil } +func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tagSlugs string) []models.SingleEnvironmentVariable { + if tagSlugs == "" { + return plainTextSecrets + } + + tagSlugsMap := make(map[string]bool) + tagSlugsList := strings.Split(tagSlugs, ",") + for _, slug := range tagSlugsList { + tagSlugsMap[slug] = true + } + + filteredSecrets := []models.SingleEnvironmentVariable{} + for _, secret := range plainTextSecrets { + for _, tag := range secret.Tags { + if tagSlugsMap[tag.Slug] { + filteredSecrets = append(filteredSecrets, secret) + break + } + } + } + + return filteredSecrets +} + func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) { - var infisicalToken string if params.InfisicalToken == "" { - infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) - } else { - infisicalToken = params.InfisicalToken + params.InfisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) } isConnected := CheckIsConnectedToInternet() @@ -233,7 +257,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo // var serviceTokenDetails api.GetServiceTokenDetailsResponse var errorToReturn error - if infisicalToken == "" { + if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" { if isConnected { log.Debug().Msg("GetAllEnvironmentVariables: Connected to internet, checking logged in creds") @@ -279,12 +303,6 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo infisicalDotJson.WorkspaceId = params.WorkspaceId } - // // Verify environment - // err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials) - // if err != nil { - // return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) - // } - secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) @@ -305,91 +323,19 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } } else { - log.Debug().Msg("Trying to fetch secrets using service token") - secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath, params.IncludeImport) - } + if params.InfisicalToken != "" { + log.Debug().Msg("Trying to fetch secrets using service token") + secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport) + } else if params.UniversalAuthAccessToken != "" { + log.Debug().Msg("Trying to fetch secrets using universal auth") + res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport) - return secretsToReturn, errorToReturn -} - -// func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error { -// httpClient := resty.New() -// httpClient.SetAuthToken(userLoggedInDetails.JTWToken). -// SetHeader("Accept", "application/json") - -// response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId}) -// if err != nil { -// return err -// } - -// listOfEnvSlugs := []string{} -// mapOfEnvSlugs := make(map[string]interface{}) - -// for _, environment := range response.AccessibleEnvironments { -// listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug) -// mapOfEnvSlugs[environment.Slug] = environment -// } - -// _, exists := mapOfEnvSlugs[environmentName] -// if !exists { -// HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ","))) -// } - -// return nil - -// } - -func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string { - if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found { - return value - } - - for _, secret := range secrets { - if secret.Key == variableWeAreLookingFor { - regex := regexp.MustCompile(`\${([^\}]*)}`) - variablesToPopulate := regex.FindAllString(secret.Value, -1) - - // case: variable is a constant so return its value - if len(variablesToPopulate) == 0 { - return secret.Value - } - - valueToEdit := secret.Value - for _, variableWithSign := range variablesToPopulate { - variableWithoutSign := strings.Trim(variableWithSign, "}") - variableWithoutSign = strings.Trim(variableWithoutSign, "${") - - // case: reference to self - if variableWithoutSign == secret.Key { - hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign - continue - } else { - var expandedVariableValue string - - if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found { - expandedVariableValue = preComputedVariable - } else { - expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs) - hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue - } - - // If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it - if _, found := hashMapOfSelfRefs[variableWithoutSign]; found { - continue - } else { - valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue) - } - } - } - - return valueToEdit - - } else { - continue + errorToReturn = err + secretsToReturn = res.Secrets } } - return "${" + variableWeAreLookingFor + "}" + return secretsToReturn, errorToReturn } var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`) @@ -401,7 +347,7 @@ func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs ma interpolatedVal, ok := interpolatedSecs[key] if !ok { - HandleError(fmt.Errorf("Could not find refered secret - %s", key), "Kindly check whether its provided") + HandleError(fmt.Errorf("could not find refered secret - %s", key), "Kindly check whether its provided") } refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1) @@ -440,7 +386,7 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod return secretMapByName } -func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken string, projectConfigPathDir string) []models.SingleEnvironmentVariable { +func ExpandSecrets(secrets []models.SingleEnvironmentVariable, auth models.ExpandSecretsAuthentication, projectConfigPathDir string) []models.SingleEnvironmentVariable { expandedSecs := make(map[string]string) interpolatedSecs := make(map[string]string) // map[env.secret-path][keyname]Secret @@ -472,8 +418,20 @@ func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken st uniqKey := fmt.Sprintf("%s.%s", env, secPathDot) if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok { + + var refSecs []models.SingleEnvironmentVariable + var err error + // if not in cross reference cache, fetch it from server - refSecs, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: infisicalToken, SecretsPath: secPath}, projectConfigPathDir) + if auth.InfisicalToken != "" { + refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: auth.InfisicalToken, SecretsPath: secPath}, projectConfigPathDir) + } else if auth.UniversalAuthAccessToken != "" { + refSecs, err = GetAllEnvironmentVariables((models.GetAllSecretsParameters{Environment: env, UniversalAuthAccessToken: auth.UniversalAuthAccessToken, SecretsPath: secPath, WorkspaceId: sec.WorkspaceId}), projectConfigPathDir) + } else if IsLoggedIn() { + refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, SecretsPath: secPath}, projectConfigPathDir) + } else { + HandleError(errors.New("no authentication provided"), "Please provide authentication to fetch secrets") + } if err != nil { HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid") } @@ -481,6 +439,7 @@ func ExpandSecrets(secrets []models.SingleEnvironmentVariable, infisicalToken st // save it to avoid calling api again for same environment and folder path crossEnvRefSecs[uniqKey] = refSecsByKey return refSecsByKey[secKey].Value + } else { return crossRefSec[secKey].Value } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f95810777e..f07aeb1903 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -86,6 +86,7 @@ services: environment: - NODE_ENV=development - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable + - TELEMETRY_ENABLED=false volumes: - ./backend/src:/app/src @@ -125,9 +126,38 @@ services: ports: - 1025:1025 # SMTP server - 8025:8025 # Web UI + + openldap: # note: more advanced configuration is available + image: osixia/openldap:1.5.0 + restart: always + environment: + LDAP_ORGANISATION: Acme + LDAP_DOMAIN: acme.com + LDAP_ADMIN_PASSWORD: admin + ports: + - 389:389 + - 636:636 + volumes: + - ldap_data:/var/lib/ldap + - ldap_config:/etc/ldap/slapd.d + profiles: [ldap] + + phpldapadmin: # username: cn=admin,dc=acme,dc=com, pass is admin + image: osixia/phpldapadmin:latest + restart: always + environment: + - PHPLDAPADMIN_LDAP_HOSTS=openldap + - PHPLDAPADMIN_HTTPS=false + ports: + - 6433:80 + depends_on: + - openldap + profiles: [ldap] volumes: postgres-data: driver: local redis_data: driver: local + ldap_data: + ldap_config: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5861a4a0e2..86a8e4cca3 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -4,10 +4,12 @@ services: db-migration: container_name: infisical-db-migration depends_on: - - db + db: + condition: service_healthy image: infisical/infisical:latest-postgres env_file: .env command: npm run migration:latest + pull_policy: always networks: - infisical @@ -16,12 +18,13 @@ services: restart: unless-stopped depends_on: db: - condition: service_started + condition: service_healthy redis: condition: service_started db-migration: condition: service_completed_successfully image: infisical/infisical:latest-postgres + pull_policy: always env_file: .env ports: - 80:8080 @@ -49,9 +52,14 @@ services: restart: always env_file: .env volumes: - - pg_data:/data/db + - pg_data:/var/lib/postgresql/data networks: - infisical + healthcheck: + test: "pg_isready --username=${POSTGRES_USER} && psql --username=${POSTGRES_USER} --list" + interval: 5s + timeout: 10s + retries: 10 volumes: pg_data: diff --git a/docs/api-reference/endpoints/folders/delete.mdx b/docs/api-reference/endpoints/folders/delete.mdx index dc73a41da0..a106cc2eba 100644 --- a/docs/api-reference/endpoints/folders/delete.mdx +++ b/docs/api-reference/endpoints/folders/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/folders/{folderId}" +openapi: "DELETE /api/v1/folders/{folderIdOrName}" --- 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 0000000000..a8a7d0430e --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/create-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Project" +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 0000000000..6b5675c4b0 --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/delete-workspace.mdx @@ -0,0 +1,8 @@ +--- +title: "Delete Project" +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 0000000000..edd0a0276f --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/get-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Project" +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 0000000000..a09c586ca8 --- /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 0000000000..8b781a4e8a --- /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 0000000000..699e3e3afe --- /dev/null +++ b/docs/api-reference/endpoints/workspaces/update-workspace.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Project" +openapi: "PATCH /api/v1/workspace/{workspaceId}" +--- \ No newline at end of file diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index ca62fe94b9..d73c0bb144 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -4,7 +4,22 @@ title: "Changelog" The changelog below reflects new product developments and updates on a monthly basis. -## January 2024 +## Feb 2024 +- Added org-scoped authentication enforcement for SAML +- Added support for [SCIM](https://infisical.com/docs/documentation/platform/scim/overview) along with instructions for setting it up with [Okta](https://infisical.com/docs/documentation/platform/scim/okta), [Azure](https://infisical.com/docs/documentation/platform/scim/azure), and [JumpCloud](https://infisical.com/docs/documentation/platform/scim/jumpcloud). +- Pushed out project update for non-E2EE w/ new endpoints like for project creation and member invitation. +- Added API Integration testing for new backend. +- Added capability to create projects in Terraform. +- Added slug-based capabilities to both organizations and projects to gradually make the API more developer-friendly moving forward. +- Fixed + improved various analytics/telemetry-related items. +- Fixed various issues associated with the Python SDK: build during installation on Mac OS, Rust dependency. +- Updated self-hosting documentation to reflect [new backend](https://infisical.com/docs/self-hosting/overview). +- Released [Postgres-based Infisical helm chart](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/infisical-standalone/). +- Added checks to ensure that breaking API changes don't get released. +- Automated API reference documentation to be inline with latest releases of Infisical. + +## Jan 2024 +- Completed Postgres migration initiative with restructed Fastify-based backend. - Reduced size of Infisical Node.js SDK by β‰ˆ90%. - Added secret fallback support to all SDK's. - Added Machine Identity support to [Terraform Provider](https://github.com/Infisical/terraform-provider-infisical). @@ -12,21 +27,21 @@ The changelog below reflects new product developments and updates on a monthly b - Added symmetric encryption support to all SDK's. - Fixed secret reminders bug, where reminders were not being updated correctly. -## December 2023 +## Dec 2023 - Released [(machine) identities](https://infisical.com/docs/documentation/platform/identities/overview) and [universal auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) features. - Created new cross-language SDKs for [Python](https://infisical.com/docs/sdks/languages/python), [Node](https://infisical.com/docs/sdks/languages/node), and [Java](https://infisical.com/docs/sdks/languages/java). - Released first version of the [Infisical Agent](https://infisical.com/docs/infisical-agent/overview) - Added ability to [manage folders via CLI](https://infisical.com/docs/cli/commands/secrets). -## November 2023 +## Nov 2023 - Replaced internal [Winston](https://github.com/winstonjs/winston) with [Pino](https://github.com/pinojs/pino) logging library with external logging to AWS CloudWatch - Added admin panel to self-hosting experience. - Released [secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview) feature with preliminary support for rotating [SendGrid](https://infisical.com/docs/documentation/platform/secret-rotation/sendgrid), [PostgreSQL/CockroachDB](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), and [MySQL/MariaDB](https://infisical.com/docs/documentation/platform/secret-rotation/mysql) credentials. - Released secret reminders feature. -## October 2023 +## Oct 2023 - Added support for [GitLab SSO](https://infisical.com/docs/documentation/platform/sso/gitlab). - Became SOC 2 (Type II) certified. @@ -35,7 +50,7 @@ The changelog below reflects new product developments and updates on a monthly b - Added native [Hasura Cloud integration](https://infisical.com/docs/integrations/cloud/hasura-cloud). - Updated resource deletion logic for user, organization, and project deletion. -## September 2023 +## Sep 2023 - Released [secret approvals](https://infisical.com/docs/documentation/platform/pr-workflows) feature. - Released an update to access controls; every user role now clearly defines and enforces a certain set of conditions across Infisical. @@ -43,7 +58,7 @@ The changelog below reflects new product developments and updates on a monthly b - Added a native integration with [Qovery](https://infisical.com/docs/integrations/cloud/qovery). - Added service token generation capability for the CLI. -## August 2023 +## Aug 2023 - Release Audit Logs V2. - Add support for [GitHub SSO](https://infisical.com/docs/documentation/platform/sso/github). @@ -171,7 +186,7 @@ The changelog below reflects new product developments and updates on a monthly b - Added sorting capability to sort keys by name alphabetically in dashboard. - Added downloading secrets back as `.env` file capability. -## August 2022 +## Aug 2022 - Released first version of the Infisical platform with push/pull capability and end-to-end encryption. - Improved security handling of authentication tokens by storing refresh tokens in HttpOnly cookies. diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index 8e2183ad96..cf76f18424 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -8,24 +8,28 @@ infisical secrets ``` ## Description + This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment. -### Sub-commands +### Sub-commands + Use this command to print out all of the secrets in your project - ```bash - $ infisical secrets - ``` +```bash +$ infisical secrets +``` + +### Environment variables - ### Environment variables Used to fetch secrets via a [service token](/documentation/platform/token) apposed to logged in credentials. Simply, export this variable in the terminal before running this command. ```bash - # Example + # Example export INFISICAL_TOKEN=st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec ``` + @@ -34,22 +38,26 @@ This command enables you to perform CRUD (create, read, update, delete) operatio To use, simply export this variable in the terminal before running this command. ```bash - # Example + # Example export INFISICAL_DISABLE_UPDATE_CHECK=true ``` + - ### Flags +### Flags + Parse shell parameter expansions in your secrets Default value: `true` + Used to select the environment name on which actions should be taken on Default value: `dev` + The `--path` flag indicates which project folder secrets will be injected from. @@ -58,6 +66,7 @@ This command enables you to perform CRUD (create, read, update, delete) operatio # Example infisical secrets --path="/" --env=dev ``` + @@ -65,38 +74,55 @@ This command enables you to perform CRUD (create, read, update, delete) operatio This command allows you selectively print the requested secrets by name - ```bash - $ infisical secrets get ... +```bash +$ infisical secrets get ... - # Example - $ infisical secrets get DOMAIN +# Example +$ infisical secrets get DOMAIN - ``` +``` + +### Flags - ### Flags Used to select the environment name on which actions should be taken on Default value: `dev` + + + + + Used to print the plain value of a single requested secret without any table style. + + Default value: `false` + + Example: `infisical secrets get DOMAIN --raw-value` + + + When running in CI/CD environments or in a script, set `INFISICAL_DISABLE_UPDATE_CHECK` env to `true`. This will help hide any CLI update messages and only show the secret value. + + -This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value. +This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value. If the secret key does not exist, a new secret will be created using both the key and value provided. ```bash $ infisical secrets set ... -## Example +## Example $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe ``` - ### Flags +### Flags + Used to select the environment name on which actions should be taken on Default value: `dev` + Used to select the project folder in which the secrets will be set. This is useful when creating new secrets under a particular path. @@ -105,43 +131,48 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb # Example infisical secrets set DOMAIN=example.com --path="common/backend" ``` + This command allows you to delete secrets by their name(s). - ```bash - $ infisical secrets delete ... +```bash +$ infisical secrets delete ... - ## Example - $ infisical secrets delete STRIPE_API_KEY DOMAIN HASH - ``` +## Example +$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH +``` + +### Flags - ### Flags Used to select the environment name on which actions should be taken on Default value: `dev` + - The `--path` flag indicates which project folder secrets will be injected from. + The `--path` flag indicates which project folder secrets will be injected from. ```bash # Example infisical secrets delete ... --path="/" ``` + This command allows you to fetch, create and delete folders from within a path from a given project. - ```bash - $ infisical secrets folders - ``` +```bash +$ infisical secrets folders +``` + +### sub commands - ### sub commands Used to fetch all folders within a path in a given project ``` @@ -179,6 +210,7 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb Default value: `` + @@ -194,10 +226,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb - Name of the folder to be deleted within selected `--path` + Name of the folder to be deleted within selected `--path` Default value: `` + @@ -210,14 +243,16 @@ To place default values in your example .env file, you can simply include the sy ```bash $ infisical secrets generate-example-env -## Example +## Example $ infisical secrets generate-example-env > .example-env ``` - ### Flags +### Flags + Used to select the environment name on which actions should be taken on Default value: `dev` + diff --git a/docs/cli/scanning-overview.mdx b/docs/cli/scanning-overview.mdx index 0163d5dcaa..748c9d4e1b 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/cli/usage.mdx b/docs/cli/usage.mdx index 2ea7b6425c..aa60c3db16 100644 --- a/docs/cli/usage.mdx +++ b/docs/cli/usage.mdx @@ -135,3 +135,38 @@ Another option to point the CLI to your self hosted Infisical instance is to set infisical --domain="https://your-self-hosted-infisical.com/api" ``` + +## History + +Your terminal keeps a history with the commands you run. When you create Infisical secrets directly from your terminal, they'll stay there for a while. + +For security and privacy concerns, we recommend you to configure your terminal to ignore those specific Infisical commands. + + + + + + + `$HOME/.profile` is pretty common but, you could place it under `$HOME/.profile.d/infisical.sh` or any profile file run at login + + + ```bash + cat <> $HOME/.profile && source $HOME/.profile + + # Ignoring specific Infisical CLI commands + DEFAULT_HISTIGNORE=$HISTIGNORE + export HISTIGNORE="*infisical secrets set*:$DEFAULT_HISTIGNORE" + EOF + ``` + + + + If you're on WSL, then you can use the Unix/Linux method. + + + Here's some [documentation](https://superuser.com/a/1658331) about how to clear the terminal history, in PowerShell and CMD + + + + + \ No newline at end of file diff --git a/docs/contributing/platform/developing.mdx b/docs/contributing/platform/developing.mdx index 7a43c8250b..a6675b6f67 100644 --- a/docs/contributing/platform/developing.mdx +++ b/docs/contributing/platform/developing.mdx @@ -16,49 +16,7 @@ git checkout -b MY_BRANCH_NAME ## Set up environment variables -Start by creating a .env file at the root of the Infisical directory then copy the contents of the file below into the .env file. - - - ```env - # Keys - # Required key for platform encryption/decryption ops - ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 - - # JWT - # Required secrets to sign JWT tokens - JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a - JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff - JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f - JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 - - # MongoDB - # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref - # to the MongoDB container instance or Mongo Cloud - # Required - MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin - - # Optional credentials for MongoDB container instance and Mongo-Express - MONGO_USERNAME=root - MONGO_PASSWORD=example - - # Website URL - # Required - SITE_URL=http://localhost:8080 - - # Mail/SMTP - SMTP_HOST='smtp-server' - SMTP_PORT='1025' - SMTP_NAME='local' - SMTP_USERNAME='team@infisical.com' - SMTP_PASSWORD= - ``` - - - - The pre-populated environment variable values above are meant to be used in development only. They should never be used in production. - - -View all available [environment variables](https://infisical.com/docs/self-hosting/configuration/envars) and guidance for each. +Start by creating a .env file at the root of the Infisical directory then copy the contents of the file linked [here](https://github.com/Infisical/infisical/blob/main/.env.example). View all available [environment variables](https://infisical.com/docs/self-hosting/configuration/envars) and guidance for each. ## Starting Infisical for development @@ -72,10 +30,7 @@ docker-compose -f docker-compose.dev.yml up --build --force-recreate ``` #### Access local server -Once all the services have spun up, browse to http://localhost:8080. To sign in, you may use the default credentials listed below. - -Email: `test@localhost.local` -Password: `testInfisical1` +Once all the services have spun up, browse to http://localhost:8080. #### Shutdown local server diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index 84d34e8710..4c96b5a783 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -2,7 +2,7 @@ title: "Introduction" --- -Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret management platform for storing, managing, and syncing +Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secrets management platform for storing, managing, and syncing application configuration and secrets like API keys, database credentials, and environment variables across applications and infrastructure. Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. diff --git a/docs/documentation/guides/node.mdx b/docs/documentation/guides/node.mdx index 1e00aed99d..8b78cde5e9 100644 --- a/docs/documentation/guides/node.mdx +++ b/docs/documentation/guides/node.mdx @@ -75,7 +75,7 @@ app.get("/", async (req, res) => { app.listen(PORT, async () => { // initialize client - console.log(`App listening on port ${port}`); + console.log(`App listening on port ${PORT}`); }); ``` diff --git a/docs/documentation/platform/ldap.mdx b/docs/documentation/platform/ldap.mdx new file mode 100644 index 0000000000..01237e1c9a --- /dev/null +++ b/docs/documentation/platform/ldap.mdx @@ -0,0 +1,36 @@ +--- +title: "LDAP" +description: "Log in to Infisical with LDAP" +--- + + + LDAP 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 authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol). + + + + In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**. + + Next, input your LDAP server settings. + + ![LDAP configuration](/images/platform/ldap/ldap-config.png) + + Here's some guidance for each field: + + - URL: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc. + - Bind DN: The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`. + - Bind Pass: The password to use along with `Bind DN` when performing the user search. + - Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=example,dc=com` + - CA Certificate: The CA certificate to use when verifying the LDAP server certificate. + + + Enabling LDAP allows members in your organization to log into Infisical via LDAP. + + ![LDAP toggle](/images/platform/ldap/ldap-toggle.png) + + \ No newline at end of file diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx new file mode 100644 index 0000000000..137538cffc --- /dev/null +++ b/docs/documentation/platform/ldap/general.mdx @@ -0,0 +1,34 @@ +--- +title: "General LDAP" +description: "Log in to Infisical with LDAP" +--- + + + LDAP 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 authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) + + + + In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**. + + Next, input your LDAP server settings. + + ![LDAP configuration](/images/platform/ldap/ldap-config.png) + + Here's some guidance for each field: + + - URL: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc. + - Bind DN: The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`. + - Bind Pass: The password to use along with `Bind DN` when performing the user search. + - Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=example,dc=com` + - CA Certificate: The CA certificate to use when verifying the LDAP server certificate. + + + Enabling LDAP allows members in your organization to log into Infisical via LDAP. + ![LDAP toggle](/images/platform/ldap/ldap-toggle.png) + + \ No newline at end of file diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx new file mode 100644 index 0000000000..5e7e42ca85 --- /dev/null +++ b/docs/documentation/platform/ldap/jumpcloud.mdx @@ -0,0 +1,54 @@ +--- +title: "JumpCloud LDAP" +description: "Configure JumpCloud LDAP for Logging into Infisical" +--- + + + LDAP 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. + + + + + In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the **Manual user entry** option. This user + will be used as a privileged service account to facilitate Infisical's ability to bind/search the LDAP directory. + + When creating the user, input their **First Name**, **Last Name**, **Username** (required), **Company Email** (required), and **Description**. + Also, create a password for the user. + + Next, under User Security Settings and Permissions > Permission Settings, check the box next to **Enable as LDAP Bind DN**. + + ![LDAP JumpCloud](/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png) + + + + In Infisical, head to your Organization Settings > Authentication > LDAP Configuration and select **Set up LDAP**. + + Next, input your JumpCloud LDAP server settings. + + ![LDAP configuration](/images/platform/ldap/ldap-config.png) + + Here's some guidance for each field: + + - URL: The LDAP server to connect to (`ldaps://ldap.jumpcloud.com:636`). + - Bind DN: The distinguished name of object to bind when performing the user search (`uid=,ou=Users,o=,dc=jumpcloud,dc=com`). + - Bind Pass: The password to use along with `Bind DN` when performing the user search. + - Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=,dc=jumpcloud,dc=com`). + - CA Certificate: The CA certificate to use when verifying the LDAP server certificate (instructions to obtain the certificate for JumpCloud [here](https://jumpcloud.com/support/connect-to-ldap-with-tls-ssl)). + + + When filling out the **Bind DN** and **Bind Pass** fields, refer to the username and password of the user created in Step 1. + + Also, for the **Bind DN** and **Search Base / User DN** fields, you'll want to use the organization ID that appears + in your LDAP instance **ORG DN**. + + + + Enabling LDAP allows members in your organization to log into Infisical via LDAP. + ![LDAP toggle](/images/platform/ldap/ldap-toggle.png) + + + +Resources: +- [JumpCloud Cloud LDAP Guide](https://jumpcloud.com/support/use-cloud-ldap) \ No newline at end of file diff --git a/docs/documentation/platform/ldap/overview.mdx b/docs/documentation/platform/ldap/overview.mdx new file mode 100644 index 0000000000..d19095b7ca --- /dev/null +++ b/docs/documentation/platform/ldap/overview.mdx @@ -0,0 +1,23 @@ +--- +title: "LDAP Overview" +description: "Log in to Infisical with LDAP" +--- + + LDAP 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 sales@infisical.com to purchase an enterprise license to use it. + + +You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) + +To note, configuring LDAP retains the end-to-end encrypted architecture of Infisical because we decouple the authentication and decryption steps; the LDAP server cannot and will not have access to the decryption key needed to decrypt your secrets. + +LDAP providers: + +- Active Directory +- [JumpCloud LDAP](/documentation/platform/ldap/jumpcloud) +- AWS Directory Service +- Foxpass + +Check out the general instructions for configuring LDAP [here](/documentation/platform/ldap/general). diff --git a/docs/documentation/platform/project-upgrade.mdx b/docs/documentation/platform/project-upgrade.mdx new file mode 100644 index 0000000000..8286342d02 --- /dev/null +++ b/docs/documentation/platform/project-upgrade.mdx @@ -0,0 +1,20 @@ +--- +title: "Enhancing Security and Usability: Project Upgrades" +--- + +At Infisical, we're constantly striving to elevate the security and usability standards of our platform to better serve our users. +With this commitment in mind, we're excited to introduce our latest addition, non-E2EE projects, aimed at addressing two significant issues while enhancing how clients interact with Infisical programmatically. + +Previously, users encountered a challenge where projects risked becoming inaccessible if the project creator deleted their account. +Additionally, our API lacked the capability to interact with projects without dealing with complex cryptographic operations. +These obstacles made API driven automation and collaboration a painful experience for a majority of our users. + +To overcome these limitations, our upgrade focuses on disabling end-to-end encryption (E2EE) for projects. +While this may raise eyebrows, it's important to understand that this decision is a strategic move to make Infisical easier to use and interact with. + +But what does this mean for our users? Essentially nothing, there are no changes required on your end. +Rest assured, all sensitive data remains encrypted at rest according to the latest industry standards. +Our commitment to security remains unwavering, and this upgrade is a testament to our dedication to delivering on our promises in both security and usability when it comes to secrets management. + +To increase consistency with existing and future integrations, all projects created on Infisical from now on will have end-to-end encryption (E2EE) disabled by default. +This will not only reduce confusion for end users, but will also make the Infisical API seamless to use. diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx new file mode 100644 index 0000000000..45f95e1350 --- /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 0000000000..68bb9b66f1 --- /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 0000000000..4baa198157 --- /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 0000000000..deec8b6304 --- /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/secret-reference.mdx b/docs/documentation/platform/secret-reference.mdx index 329bc4b9f6..66961db99d 100644 --- a/docs/documentation/platform/secret-reference.mdx +++ b/docs/documentation/platform/secret-reference.mdx @@ -10,7 +10,7 @@ This means that updating the value of a base secret propagates directly to other Currently, the secret referencing feature is only supported by the - [Infisical CLI](/cli/overview) and [native integrations](/integrations/overview). + [Infisical CLI](/cli/overview), [native integrations](/integrations/overview) and [Infisical Agent](/infisical-agent/overview). We intend to add support for it to the [Node SDK](https://infisical.com/docs/sdks/languages/node), [Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java) this quarter. diff --git a/docs/documentation/platform/secret-rotation/aws-iam.mdx b/docs/documentation/platform/secret-rotation/aws-iam.mdx new file mode 100644 index 0000000000..b4247af80f --- /dev/null +++ b/docs/documentation/platform/secret-rotation/aws-iam.mdx @@ -0,0 +1,143 @@ +--- +title: "AWS IAM User" +description: "Rotated access key id and secret key of AWS IAM Users" +--- + +Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical +at a specified interval or on-demand. + +## Workflow + +The typical workflow for using the AWS IAM User rotation strategy consists of four steps: + +1. Creating the target IAM user whose credentials you wish to rotate. +2. Creating the managing IAM user used by Infisical to rotate the credentials of the target IAM user. +3. Configuring the rotation strategy in Infisical with the credentials of the managing IAM user. +4. Pressing the **Rotate** button in the Infisical dashboard to trigger the rotation of the target IAM user's credentials. The strategy can also be configured to rotate the credentials automatically at a specified interval. + +In the following steps, we explore the end-to-end workflow for setting up this strategy in Infisical. + + + + To begin, create an IAM user whose credentials you wish to rotate. If you already have an IAM user, + then you can skip this step. + + + Next, create another IAM user to be used by Infisical to rotate the credentials of the IAM user in the previous step. + + 2.1. In your AWS console, head to IAM > Access management > Users and press **Create user**. + + ![iam user secret rotation create user](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-user.png) + + 2.2. Next, give the user a username like **infisical-rotation-manager** and press **Next**. + + ![iam user secret rotation username](../../../images/platform/secret-rotation/aws-iam/rotation-manager-username.png) + + 2.3. Next, in the **Set permissions** step, select **Attach policies directly** and then press **Create policy**. + + ![iam user secret rotation create policy](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-policy.png) + + 2.4. Next, in the **Policy editor**, paste the following JSON and press **Next**: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "iam:DeleteAccessKey", + "iam:GetAccessKeyLastUsed", + "iam:CreateAccessKey" + ], + "Resource": "*" + } + ] + } + ``` + + + The IAM policy above uses the wildcard option in Resource: "*". + + You may want to restrict the policy to a specific path, and make any adjustments as necessary, to control access for the managing user in production. + + Read more about this [here](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/). + + + In the **Review and create** step, give the policy a name like **infisical-rotation-manager**, press **Create policy** to finish creating the policy. + + ![iam user secret rotation policy review](../../../images/platform/secret-rotation/aws-iam/rotation-manager-policy-review.png) + + 2.5. Back in the **Set permissions** step from step 2.3, refresh the policy list and search for the policy you just created from step 2.4. + + Select the policy and press **Next**. + + ![iam user secret rotation attach policy](../../../images/platform/secret-rotation/aws-iam/rotation-manager-attach-policy.png) + + In the **Review and create** step, press **Create user** to finish creating the IAM user. + + ![iam user secret rotation manager user review](../../../images/platform/secret-rotation/aws-iam/rotation-manager-user-review.png) + + 2.5. Having created the user, head to its Security credentials > Access keys and press **Create access key**. + + Follow the subsequent steps to create the **access key** and **secret access key** credential pair for the user. + + ![iam user secret rotation manager create access key](../../../images/platform/secret-rotation/aws-iam/rotation-manager-create-access-key.png) + + At the end of the flow, copy the **Access key** and **Secret access key** to use when configuring the AWS IAM User rotation strategy back in Infisical next. + + ![iam user secret rotation manager access keys](../../../images/platform/secret-rotation/aws-iam/rotation-manager-access-keys.png) + + + 3.1. Back in Infisical, head to the Project > Secrets > Environment and path where you want the rotated AWS IAM credentials to appear and create two placeholder secrets. + + In this example, we'll create two secrets called `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`. + + ![iam user secret rotation secrets](../../../images/platform/secret-rotation/aws-iam/rotation-config-secrets.png) + + 3.2. Next, in the **Secret Rotation** tab, press on the **AWS IAM** tile to configure the AWS IAM User rotation strategy. + + ![iam user secret rotation select aws iam user method](../../../images/platform/secret-rotation/aws-iam/rotations-select-aws-iam-user.png) + + 3.3. Input the configuration details for the AWS IAM User rotation strategy obtained from steps 1 and 2: + + ![iam user secret rotation config 1](../../../images/platform/secret-rotation/aws-iam/rotation-config-1.png) + + Here's some guidance on each field: + + - Manager User Access Key: The managing IAM user's access key from step 2.5. + - Manager User Secret Key: The managing IAM user's secret access key from step 2.5. + - Manager User AWS Region: The [AWS region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) for Infisical to make requests to such as `us-east-1`. + - IAM Username: The IAM username of the user from step 1. + + Next, specify the output secret mappings configuration for the rotated AWS IAM credentials; this is the secrets whose values will be replaced with new credentials after each rotation. + Here, you can also specify a rotation interval for the credentials to be automatically rotated periodically. + + In this example, we want to map the output of the rotated AWS IAM credentials to the secrets that we created in step 3.1 (i.e. `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY`). + + ![iam user secret rotation config 2](../../../images/platform/secret-rotation/aws-iam/rotation-config-2.png) + + Finally, press **Submit** to create the secret rotation strategy. + + + You should now see the AWS IAM User rotation strategy listed in the **Secret Rotation** tab. + + To manually trigger a rotation, you can press the **Rotate** button on the strategy. + Once triggered, the secrets in step 3.1 should be updated with new rotated credential values. + + ![iam user secret rotations aws iam user](../../../images/platform/secret-rotation/aws-iam/rotations-aws-iam-user.png) + + + +**FAQ** + + + + There are a few reasons for why this might happen: + + - The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target IAM username is incorrect, etc.). + - The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup [paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary permissions to rotate the credentials. + - The target IAM user already has 2 access keys configured in AWS; you should delete one of the access keys to allow for rotation. + + \ No newline at end of file diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx index f70e8a70b7..b11ae1d76a 100644 --- a/docs/documentation/platform/secret-rotation/postgres.mdx +++ b/docs/documentation/platform/secret-rotation/postgres.mdx @@ -1,21 +1,17 @@ --- title: "PostgreSQL/CockroachDB" -description: "Rotated database user password of a postgreSQL or cockroach db" +description: "Rotated database user password of a PostgreSQL or Cockroach DB" --- Infisical will update periodically the provided database user's password. - - At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud. - - ## Working -1. User's has to create the two user's for Infisical to rotate and provide them required database access -2. Infisical will connect with your database with admin access -3. If last rotated one was username1, then username2 is chosen to be rotated -5. Update it's password with random value -6. After testing it gets saved to the provided secret mapping +1. User's has to create the two user's for Infisical to rotate and provide them required database access. +2. Infisical will connect with your database with admin access. +3. If last rotated one was username1, then username2 is chosen to be rotated. +5. Update it's password with random value. +6. After testing it gets saved to the provided secret mapping. ## Rotation Configuration @@ -34,4 +30,4 @@ Infisical will update periodically the provided database user's password. - Finally select the secrets in your provided board to replace with new secret after each rotation - Your done and good to go. -Congrats. You have 10x your PostgreSQL/CockroachDB access security. +Congratulations. You have improved your PostgreSQL/CockroachDB access security. diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 0f644834ab..30622e6e82 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -4,7 +4,7 @@ description: "Configure Azure SAML for Infisical SSO" --- - Azure SAML SSO feature is a paid feature. + Azure SAML SSO is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it. @@ -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/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx new file mode 100644 index 0000000000..743c4e3ff8 --- /dev/null +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -0,0 +1,95 @@ +--- +title: "Google SAML" +description: "Configure Google SAML for Infisical SSO" +--- + + + Google SAML SSO feature is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + + + + In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Set up SAML SSO**. + + Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application. + + ![Google SAML initial configuration](../../../images/sso/google-saml/init-config.png) + + + 2.1. In your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps and + create a **custom SAML app**. + + ![Google SAML app creation](../../../images/sso/google-saml/create-custom-saml-app.png) + + 2.2. In the **App details** tab, give the application a unique name like Infisical. + + ![Google SAML app naming](../../../images/sso/google-saml/name-custom-saml-app.png) + + 2.3. In the **Google Identity Provider details** tab, copy the **SSO URL**, **Entity ID** and **Certificate**. + + ![Google SAML custom app details](../../../images/sso/google-saml/custom-saml-app-config.png) + + 2.4. Back in Infisical, set **SSO URL**, **IdP Entity ID**, and **Certificate** to the corresponding items from step 2.3. + + ![Google SAML Infisical config](../../../images/sso/google-saml/infisical-config.png) + + 2.5. Back in the Google Admin console, in the **Service provider details** tab, set the **ACS URL** and **Entity ID** to the corresponding items from step 1. + + Also, check the **Signed response** checkbox. + + ![Google SAML app config 2](../../../images/sso/google-saml/custom-saml-app-config-2.png) + + 2.6. In the **Attribute mapping** tab, configure the following map: + + - **First name** -> **firstName** + - **Last name** -> **lastName** + - **Primary email** -> **email** + + ![Google SAML attribute mapping](../../../images/sso/google-saml/attribute-mapping.png) + + Click **Finish**. + + + Back in your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps > your SAML app + and press on **User access**. + + ![Google SAML user access](../../../images/sso/google-saml/user-access.png) + + To assign everyone in your organization to the application, click **On for everyone** or **Off for everyone** and then click **Save**. + + You can also assign an organizational unit or set of users to an application; you can learn more about that [here](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app). + + ![Google SAML user access assignment](../../../images/sso/google-saml/user-access-assign.png) + + + Enabling SAML SSO allows members in your organization to log into Infisical via Google Workspace. + + ![Google SAML enable](../../../images/sso/google-saml/enable-saml.png) + + + Enforcing SAML SSO ensures that members in your organization can only access Infisical + by logging into the organization via Google. + + To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Google 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 Google + prior to enforcing SAML SSO to prevent any unintended issues. + + + + + + If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to + set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: + + - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. + - `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) + + +References: +- Google's guide to [set up your own custom SAML app](https://support.google.com/a/answer/6087519?hl=en#add_custom_saml&turn_on&verify_sso&&zippy=%2Cstep-add-the-custom-saml-app%2Cstep-turn-on-your-saml-app%2Cstep-verify-that-sso-is-working-with-your-custom-app). \ No newline at end of file diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index 2273a8852a..8b64c8643c 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -4,7 +4,7 @@ description: "Configure JumpCloud SAML for Infisical SSO" --- - JumpCloud SAML SSO feature is a paid feature. + JumpCloud SAML SSO is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it. @@ -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 576bd769a0..f182039639 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -4,7 +4,7 @@ description: "Configure Okta SAML 2.0 for Infisical SSO" --- - Okta SAML SSO feature is a paid feature. + Okta SAML SSO is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it. @@ -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 8f4b3bb0f2..e1fd259573 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). @@ -22,3 +22,4 @@ your IdP cannot and will not have access to the decryption key needed to decrypt - [Okta SAML](/documentation/platform/sso/okta) - [Azure SAML](/documentation/platform/sso/azure) - [JumpCloud SAML](/documentation/platform/sso/jumpcloud) +- [Google SAML](/documentation/platform/sso/google-saml) diff --git a/docs/images/docker-swarm-secrets-complete.png b/docs/images/docker-swarm-secrets-complete.png new file mode 100644 index 0000000000..28b439445e Binary files /dev/null and b/docs/images/docker-swarm-secrets-complete.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku-create.png b/docs/images/integrations/heroku/integrations-heroku-create.png index a2a8d4e767..452dc51593 100644 Binary files a/docs/images/integrations/heroku/integrations-heroku-create.png and b/docs/images/integrations/heroku/integrations-heroku-create.png differ diff --git a/docs/images/integrations/heroku/integrations-heroku.png b/docs/images/integrations/heroku/integrations-heroku.png index 31c8284cd7..ead3324474 100644 Binary files a/docs/images/integrations/heroku/integrations-heroku.png and b/docs/images/integrations/heroku/integrations-heroku.png differ diff --git a/docs/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png b/docs/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png new file mode 100644 index 0000000000..b50c1e0be6 Binary files /dev/null and b/docs/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png differ diff --git a/docs/images/platform/ldap/jumpcloud/ldap-jumpcloud-org-dn.png b/docs/images/platform/ldap/jumpcloud/ldap-jumpcloud-org-dn.png new file mode 100644 index 0000000000..cd6166b1f5 Binary files /dev/null and b/docs/images/platform/ldap/jumpcloud/ldap-jumpcloud-org-dn.png differ diff --git a/docs/images/platform/ldap/ldap-config.png b/docs/images/platform/ldap/ldap-config.png new file mode 100644 index 0000000000..8d105c1d61 Binary files /dev/null and b/docs/images/platform/ldap/ldap-config.png differ diff --git a/docs/images/platform/ldap/ldap-toggle.png b/docs/images/platform/ldap/ldap-toggle.png new file mode 100644 index 0000000000..dcc7ffc960 Binary files /dev/null and b/docs/images/platform/ldap/ldap-toggle.png differ 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 0000000000..5255c3a8d1 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 0000000000..c97574673a 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 0000000000..d457a1170c 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 0000000000..2654f86fec 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 0000000000..949474a49a 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 0000000000..b96ab6cf75 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 0000000000..b10fd099d6 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 0000000000..1c42729a7b 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 0000000000..e1980fdfb4 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 0000000000..a3ea836ec5 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 0000000000..97ad34567d 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 0000000000..b20ceddca0 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 0000000000..d5688182eb 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 0000000000..f1c2e92211 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 0000000000..d3a4586c28 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 0000000000..9fe4eac3e9 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 0000000000..a4385244f6 Binary files /dev/null and b/docs/images/platform/scim/scim-enable-provisioning.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-config-1.png b/docs/images/platform/secret-rotation/aws-iam/rotation-config-1.png new file mode 100644 index 0000000000..2e6e5b8302 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-config-1.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-config-2.png b/docs/images/platform/secret-rotation/aws-iam/rotation-config-2.png new file mode 100644 index 0000000000..487147c478 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-config-2.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-config-secrets.png b/docs/images/platform/secret-rotation/aws-iam/rotation-config-secrets.png new file mode 100644 index 0000000000..8a15d486fd Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-config-secrets.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-access-key-third-party.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-access-key-third-party.png new file mode 100644 index 0000000000..8e01b60f57 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-access-key-third-party.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-access-keys.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-access-keys.png new file mode 100644 index 0000000000..f30b69dad1 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-access-keys.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-attach-policy.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-attach-policy.png new file mode 100644 index 0000000000..4944366de4 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-attach-policy.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-access-key.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-access-key.png new file mode 100644 index 0000000000..7a71e4a5e9 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-access-key.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-policy.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-policy.png new file mode 100644 index 0000000000..46ae782e12 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-policy.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-user.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-user.png new file mode 100644 index 0000000000..05542dae3f Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-create-user.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-policy-review.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-policy-review.png new file mode 100644 index 0000000000..ae81055b09 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-policy-review.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-user-review.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-user-review.png new file mode 100644 index 0000000000..34773f9136 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-user-review.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotation-manager-username.png b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-username.png new file mode 100644 index 0000000000..573c3cf942 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotation-manager-username.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotations-aws-iam-user.png b/docs/images/platform/secret-rotation/aws-iam/rotations-aws-iam-user.png new file mode 100644 index 0000000000..5ea395e688 Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotations-aws-iam-user.png differ diff --git a/docs/images/platform/secret-rotation/aws-iam/rotations-select-aws-iam-user.png b/docs/images/platform/secret-rotation/aws-iam/rotations-select-aws-iam-user.png new file mode 100644 index 0000000000..17bbe3b1ff Binary files /dev/null and b/docs/images/platform/secret-rotation/aws-iam/rotations-select-aws-iam-user.png differ diff --git a/docs/images/self-hosting/applicable-to-all/selfhost-signup.png b/docs/images/self-hosting/applicable-to-all/selfhost-signup.png new file mode 100644 index 0000000000..745c32a448 Binary files /dev/null and b/docs/images/self-hosting/applicable-to-all/selfhost-signup.png differ diff --git a/docs/images/self-hosting/guides/mongo-postgres/mongo-migration.png b/docs/images/self-hosting/guides/mongo-postgres/mongo-migration.png new file mode 100644 index 0000000000..b74cd49631 Binary files /dev/null and b/docs/images/self-hosting/guides/mongo-postgres/mongo-migration.png differ diff --git a/docs/images/sso/google-saml/attribute-mapping.png b/docs/images/sso/google-saml/attribute-mapping.png new file mode 100644 index 0000000000..b5702cd2ba Binary files /dev/null and b/docs/images/sso/google-saml/attribute-mapping.png differ diff --git a/docs/images/sso/google-saml/create-custom-saml-app.png b/docs/images/sso/google-saml/create-custom-saml-app.png new file mode 100644 index 0000000000..6139932f13 Binary files /dev/null and b/docs/images/sso/google-saml/create-custom-saml-app.png differ diff --git a/docs/images/sso/google-saml/custom-saml-app-config-2.png b/docs/images/sso/google-saml/custom-saml-app-config-2.png new file mode 100644 index 0000000000..9839dd0c4d Binary files /dev/null and b/docs/images/sso/google-saml/custom-saml-app-config-2.png differ diff --git a/docs/images/sso/google-saml/custom-saml-app-config.png b/docs/images/sso/google-saml/custom-saml-app-config.png new file mode 100644 index 0000000000..8f4ad5928d Binary files /dev/null and b/docs/images/sso/google-saml/custom-saml-app-config.png differ diff --git a/docs/images/sso/google-saml/enable-saml.png b/docs/images/sso/google-saml/enable-saml.png new file mode 100644 index 0000000000..7a90eed55c Binary files /dev/null and b/docs/images/sso/google-saml/enable-saml.png differ diff --git a/docs/images/sso/google-saml/infisical-config.png b/docs/images/sso/google-saml/infisical-config.png new file mode 100644 index 0000000000..250b4ed37c Binary files /dev/null and b/docs/images/sso/google-saml/infisical-config.png differ diff --git a/docs/images/sso/google-saml/init-config.png b/docs/images/sso/google-saml/init-config.png new file mode 100644 index 0000000000..c4b967e547 Binary files /dev/null and b/docs/images/sso/google-saml/init-config.png differ diff --git a/docs/images/sso/google-saml/name-custom-saml-app.png b/docs/images/sso/google-saml/name-custom-saml-app.png new file mode 100644 index 0000000000..580896d05b Binary files /dev/null and b/docs/images/sso/google-saml/name-custom-saml-app.png differ diff --git a/docs/images/sso/google-saml/user-access-assign.png b/docs/images/sso/google-saml/user-access-assign.png new file mode 100644 index 0000000000..afa115c65b Binary files /dev/null and b/docs/images/sso/google-saml/user-access-assign.png differ diff --git a/docs/images/sso/google-saml/user-access.png b/docs/images/sso/google-saml/user-access.png new file mode 100644 index 0000000000..bd69c22775 Binary files /dev/null and b/docs/images/sso/google-saml/user-access.png differ diff --git a/docs/infisical-agent/guides/docker-swarm-with-agent.mdx b/docs/infisical-agent/guides/docker-swarm-with-agent.mdx new file mode 100644 index 0000000000..8ab4ca9625 --- /dev/null +++ b/docs/infisical-agent/guides/docker-swarm-with-agent.mdx @@ -0,0 +1,164 @@ +--- +title: 'Docker Swarm' +description: "How to manage secrets in Docker Swarm services" +--- + +In this guide, we'll demonstrate how to use Infisical for managing secrets within Docker Swarm. +Specifically, we'll set up a sidecar container using the [Infisical Agent](/infisical-agent/overview), which authenticates with Infisical to retrieve secrets and access tokens. +These secrets are then stored in a shared volume accessible by other services in your Docker Swarm. + +## Prerequisites +- Infisical account +- Docker version 20.10.24 or newer +- Basic knowledge of Docker Swarm +- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed on your system +- Familiarity with the [Infisical Agent](/infisical-agent/overview) + +## Objective +Our goal is to deploy an Nginx instance in your Docker Swarm cluster, configured to display Infisical secrets on its landing page. This will provide hands-on experience in fetching and utilizing secrets from Infisical within Docker Swarm. The principles demonstrated here are also applicable to Docker Compose deployments. + + + + Start by cloning the [Infisical guide assets repository](https://github.com/Infisical/infisical-guides.git) from Github. This repository includes necessary assets for this and other Infisical guides. Focus on the `docker-swarm-with-agent` sub-directory, which we'll use as our working directory. + + + + To allow the agent to fetch your Infisical secrets, choose an authentication method for the agent. For this guide, we will use [Universal Auth](/documentation/platform/identities/universal-auth) for authentication. Follow the instructions [here](/documentation/platform/identities/universal-auth) to generate a client ID and client secret. + + + + Copy the client ID and client secret obtained in the previous step into the `client-id` and `client-secret` text files, respectively. + + + + The Infisical Agent will authenticate using Universal Auth and retrieve secrets for rendering as specified in the template(s). + Adjust the `polling-interval` to control the frequency of secret updates. + + In the example template, the secrets are rendered as an HTML page, which will be set as Nginx's home page to demonstrate successful secret retrieval and utilization. + + + Remember to add your project id, environment slug and path of corresponding Infisical project to the secret template. + + + + ```yaml infisical-agent-config + infisical: + address: "https://app.infisical.com" + auth: + type: "universal-auth" + config: + client-id: "/run/secrets/infisical-universal-auth-client-id" + client-secret: "/run/secrets/infisical-universal-auth-client-secret" + remove_client_secret_on_read: false + sinks: + - type: "file" + config: + path: "/infisical-secrets/access-token" + templates: + - source-path: /run/secrets/nginx-home-page-template + destination-path: /infisical-secrets/index.html + config: + polling-interval: 60s + ``` + + Some paths contain `/run/secrets/` because the contents of those files reside in a [Docker secret](https://docs.docker.com/engine/swarm/secrets/#how-docker-manages-secrets). + + + + ```html nginx-home-page-template + + + +

This file is rendered by Infisical agent template engine

+

Here are the secrets that have been fetched from Infisical and stored in your volume mount

+
    + {{- with secret "7df67a5f-d26a-4988-a375-7153c08149da" "dev" "/" }} + {{- range . }} +
  1. {{ .Key }}={{ .Value }}
  2. + {{- end }} + {{- end }} +
+ + + ``` +
+
+
+ + + Define the `infisical-agent` and `nginx` services in your Docker Compose file. `infisical-agent` will handle secret retrieval and storage. These secrets are stored in a volume, accessible by other services like Nginx. + + ```yaml docker-compose.yaml + version: "3.1" + + services: + infisical-agent: + container_name: infisical-agnet + image: infisical/cli:0.18.0 + command: agent --config=/run/secrets/infisical-agent-config + volumes: + - infisical-agent:/infisical-secrets + secrets: + - infisical-universal-auth-client-id + - infisical-universal-auth-client-secret + - infisical-agent-config + - nginx-home-page-template + networks: + - infisical_network + + nginx: + image: nginx:latest + ports: + - "80:80" + volumes: + - infisical-agent:/usr/share/nginx/html + networks: + - infisical_network + + volumes: + infisical-agent: + + secrets: + infisical-universal-auth-client-id: + file: ./client-id + infisical-universal-auth-client-secret: + file: ./client-secret + infisical-agent-config: + file: ./infisical-agent-config + nginx-home-page-template: + file: ./nginx-home-page-template + + + networks: + infisical_network: + ``` + + + + ``` + docker swarm init + ``` + + + + ``` + docker stack deploy -c docker-compose.yaml agent-demo + ``` + + + + To confirm that secrets are properly rendered and accessible, navigate to `http://localhost`. You should see the Infisical secrets displayed on the Nginx landing page. + + ![Nginx displaying Infisical secrets](/images/docker-swarm-secrets-complete.png) + + + + ``` + docker stack rm agent-demo + ``` + +
+ +## Considerations +- Secret Updates: Applications that access secrets directly from the volume mount will receive updates in real-time, in accordance with the `polling-interval` set in agent config. +- In-Memory Secrets: If your application loads secrets into memory, the new secrets will be available to the application on the next deployment. diff --git a/docs/infisical-agent/overview.mdx b/docs/infisical-agent/overview.mdx index c98090289c..9265d9bfe3 100644 --- a/docs/infisical-agent/overview.mdx +++ b/docs/infisical-agent/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Infisical Agent" +title: "Overview" +description: "This page describes how to manage secrets using Infisical Agent." --- Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical. @@ -51,6 +52,9 @@ While specifying an authentication method is mandatory to start the agent, confi | `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | | `templates[].source-path` | The path to the template file that should be used to render secrets. | | `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | +| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `60s` (optional) | +| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | +| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | ## Quick start Infisical Agent @@ -76,6 +80,11 @@ sinks: templates: - source-path: my-dot-ev-secret-template destination-path: /some/path/.env + config: + polling-interval: 60s + execute: + timeout: 30 + command: ./reload-app.sh ``` Above is an example agent configuration file that defines the token authentication method, one sink location (where to deposit access tokens after renewal) and a secret template. diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index 2d8fdc4456..903ab82701 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -30,6 +30,17 @@ description: "How to sync secrets from Infisical to Heroku" Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku. ![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png) + + Here's some guidance on each field: + + - Project Environment: The environment in the current Infisical project from which you want to sync secrets from. + - Secrets Path: The path in the current Infisical project from which you want to sync secrets from such as `/` (for secrets that do not reside in a folder) or `/foo/bar` (for secrets nested in a folder, in this case a folder called `bar` in another folder called `foo`). + - Heroku App: The application in Heroku that you want to sync secrets to. + - Initial Sync Behavior (default is **Import - Prefer values from Infisical**): The behavior of the first sync operation triggered after creating the integration. + - **No Import - Overwrite all values in Heroku**: Sync secrets and overwrite any existing secrets in Heroku. + - **Import - Prefer values from Infisical**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, do nothing. Afterwards, sync secrets to Heroku. + - **Import - Prefer values from Heroku**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, replace its value with the one from Heroku. Afterwards, sync secrets to Heroku. + ![integrations heroku](../../images/integrations/heroku/integrations-heroku.png) diff --git a/docs/integrations/platforms/ansible.mdx b/docs/integrations/platforms/ansible.mdx index efb9e4f638..2d524d55c4 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-intro.mdx b/docs/integrations/platforms/docker-intro.mdx index 5f23584c85..2823fc8ca6 100644 --- a/docs/integrations/platforms/docker-intro.mdx +++ b/docs/integrations/platforms/docker-intro.mdx @@ -1,25 +1,25 @@ --- title: "Docker" -description: "Learn how to feed secrets from Infisical into your docker application" +description: "Learn how to feed secrets from Infisical into your Docker application" --- -There are many methods to inject Infisical secrets to docker-based applications. -Regardless of which method you choose, these methods will inject secrets from Infisical as environment variables into your Docker container. +There are many methods to inject Infisical secrets into Docker-based applications. +Regardless of the method you choose, they all inject secrets from Infisical as environment variables into your Docker container. Install and run your app start command with Infisical CLI - Feed secrets via `--env-file` flag in docker run command + Feed secrets with the `--env-file` flag when using the + `docker run` command - Inject secrets to multiple services using Docker Compose + Inject secrets into multiple services using Docker Compose The main difference between the "Docker Entrypoint" and "Docker run" approach is where the Infisical CLI is installed. -In most production settings, it's typically inconvenient to have the Infisical CLI installed and executed externally. -As a result, we suggest using the "Docker Entrypoint" method for production purposes. +In most production settings, it's typically less convenient to have the Infisical CLI installed and executed externally, so we suggest using the "Docker Entrypoint" method for production purposes. However, if this limitation doesn't apply to you, select the method that best fits your needs. \ No newline at end of file diff --git a/docs/integrations/platforms/docker.mdx b/docs/integrations/platforms/docker.mdx index e9682b7853..8fbf288adb 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/internals/components.mdx b/docs/internals/components.mdx index 02c6d36924..29522b0bba 100644 --- a/docs/internals/components.mdx +++ b/docs/internals/components.mdx @@ -9,9 +9,7 @@ The Infisical API (sometimes referred to as the **backend**) contains the core p ## Storage backend -Infisical relies on a storage backend to store data including users and secrets. - -Currently, the only supported storage backend is [MongoDB](https://www.mongodb.com) but we plan to add support for other options including PostgreSQL in Q1 2024. +Infisical relies on a storage backend to store data including users and secrets. Infisical's storage backend is Postgres. ## Redis @@ -27,4 +25,4 @@ Clients are any application or infrastructure that connecting to the Infisical A - Public API: Making API requests directly to the Infisical API. - Client SDK: A platform-specific library with method abstractions for working with secrets. Currently, there are three official SDKs: [Node SDK](https://infisical.com/docs/sdks/languages/node), [Python SDK](https://infisical.com/docs/sdks/languages/python), and [Java SDK](https://infisical.com/docs/sdks/languages/java). - CLI: A terminal-based interface for interacting with the Infisical API. -- Kubernetes Operator: This operator retrieves secrets from Infisical and securely store \ No newline at end of file +- Kubernetes Operator: This operator retrieves secrets from Infisical and securely store diff --git a/docs/mint.json b/docs/mint.json index b67493aeba..732c0f0369 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -39,32 +39,32 @@ "name": "Start for Free", "url": "https://app.infisical.com/signup" }, - "anchors": [ + "tabs": [ { - "name": "Internals", - "icon": "sitemap", - "url": "internals" - }, - { - "name": "SDKs", - "icon": "puzzle-piece", - "url": "sdks" + "name": "Changelog", + "url": "changelog" }, { "name": "API Reference", - "icon": "cloud", "url": "api-reference" }, { - "name": "Changelog", - "icon": "timer", - "url": "changelog" + "name": "SDKs", + "url": "sdks" }, + { + "name": "Contributing", + "url": "contributing" + } + ], + "anchors": [ + { "name": "Contributing", "icon": "code", "url": "contributing" }, + { "name": "Blog", "icon": "newspaper", @@ -79,6 +79,11 @@ "name": "GitHub", "icon": "github", "url": "https://github.com/Infisical/infisical" + }, + { + "name": "Internals", + "icon": "sitemap", + "url": "internals" } ], "navigation": [ @@ -134,7 +139,8 @@ "documentation/platform/secret-rotation/overview", "documentation/platform/secret-rotation/sendgrid", "documentation/platform/secret-rotation/postgres", - "documentation/platform/secret-rotation/mysql" + "documentation/platform/secret-rotation/mysql", + "documentation/platform/secret-rotation/aws-iam" ] }, { @@ -146,7 +152,25 @@ "documentation/platform/sso/gitlab", "documentation/platform/sso/okta", "documentation/platform/sso/azure", - "documentation/platform/sso/jumpcloud" + "documentation/platform/sso/jumpcloud", + "documentation/platform/sso/google-saml" + ] + }, + { + "group": "LDAP", + "pages": [ + "documentation/platform/ldap/overview", + "documentation/platform/ldap/jumpcloud", + "documentation/platform/ldap/general" + ] + }, + { + "group": "SCIM", + "pages": [ + "documentation/platform/scim/overview", + "documentation/platform/scim/okta", + "documentation/platform/scim/azure", + "documentation/platform/scim/jumpcloud" ] } ] @@ -154,27 +178,25 @@ { "group": "Self-host Infisical", "pages": [ + "self-hosting/overview", + "self-hosting/configuration/requirements", { - "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", - "self-hosting/deployment-options/aws-ec2", - "self-hosting/deployment-options/aws-lightsail", - "self-hosting/deployment-options/gcp-cloud-run", - "self-hosting/deployment-options/azure-app-services", - "self-hosting/deployment-options/azure-container-instances", - "self-hosting/deployment-options/digital-ocean-marketplace", - "self-hosting/deployment-options/fly.io", - "self-hosting/deployment-options/railway" + "self-hosting/deployment-options/kubernetes-helm" ] }, "self-hosting/configuration/envars", - "self-hosting/configuration/email", - "self-hosting/configuration/redis", - "self-hosting/configuration/sso", + { + "group": "Guides", + "pages": [ + "self-hosting/configuration/schema-migrations", + "self-hosting/guides/mongo-to-postgres" + ] + }, + "self-hosting/ee", "self-hosting/faq" ] }, @@ -213,16 +235,27 @@ { "group": "Agent", "pages": [ - "infisical-agent/overview" + "infisical-agent/overview", + { + "group": "Use cases", + "pages": [ + "infisical-agent/guides/docker-swarm-with-agent", + "integrations/platforms/ecs-with-agent" + ] + } ] }, - { - "group": "Integrations", - "pages": ["integrations/overview"] - }, { "group": "Infrastructure Integrations", "pages": [ + { + "group": "Container orchestrators", + "pages": [ + "integrations/platforms/kubernetes", + "infisical-agent/guides/docker-swarm-with-agent", + "integrations/platforms/ecs-with-agent" + ] + }, { "group": "Docker", "pages": [ @@ -232,14 +265,12 @@ "integrations/platforms/docker-compose" ] }, - "integrations/platforms/kubernetes", "integrations/frameworks/terraform", - "integrations/platforms/ansible", - "integrations/platforms/ecs-with-agent" + "integrations/platforms/ansible" ] }, { - "group": "3rd-party Integrations", + "group": "Native Integrations", "pages": [ { "group": "AWS", @@ -252,39 +283,49 @@ "group": "Digital Ocean", "pages": ["integrations/cloud/digital-ocean-app-platform"] }, - "integrations/cloud/heroku", "integrations/cloud/vercel", - "integrations/cloud/netlify", - "integrations/cloud/render", - "integrations/cloud/railway", - "integrations/cloud/flyio", - "integrations/cloud/laravel-forge", - "integrations/cloud/supabase", - "integrations/cloud/northflank", - "integrations/cloud/hasura-cloud", - "integrations/cloud/terraform-cloud", - "integrations/cloud/cloudflare-pages", - "integrations/cloud/cloudflare-workers", - "integrations/cloud/qovery", - "integrations/cloud/hashicorp-vault", "integrations/cloud/azure-key-vault", "integrations/cloud/gcp-secret-manager", - "integrations/cloud/cloud-66", - "integrations/cloud/windmill" + { + "group": "View more", + "pages": [ + "integrations/cloud/heroku", + "integrations/cloud/netlify", + "integrations/cloud/render", + "integrations/cloud/railway", + "integrations/cloud/flyio", + "integrations/cloud/laravel-forge", + "integrations/cloud/supabase", + "integrations/cloud/northflank", + "integrations/cloud/hasura-cloud", + "integrations/cloud/terraform-cloud", + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers", + "integrations/cloud/qovery", + "integrations/cloud/hashicorp-vault", + "integrations/cloud/cloud-66", + "integrations/cloud/windmill" + ] + } ] }, { "group": "CI/CD Integrations", "pages": [ "integrations/cloud/teamcity", - "integrations/cloud/checkly", "integrations/cicd/githubactions", "integrations/cicd/gitlab", - "integrations/cicd/circleci", - "integrations/cicd/travisci", - "integrations/cicd/bitbucket", - "integrations/cicd/codefresh", - "integrations/cicd/jenkins" + { + "group": "View more", + "pages": [ + "integrations/cicd/circleci", + "integrations/cicd/travisci", + "integrations/cicd/bitbucket", + "integrations/cicd/codefresh", + "integrations/cicd/jenkins", + "integrations/cloud/checkly" + ] + } ] }, { @@ -294,20 +335,25 @@ "integrations/frameworks/react", "integrations/frameworks/vue", "integrations/frameworks/express", - "integrations/frameworks/nextjs", - "integrations/frameworks/nestjs", - "integrations/frameworks/sveltekit", - "integrations/frameworks/nuxt", - "integrations/frameworks/gatsby", - "integrations/frameworks/remix", - "integrations/frameworks/vite", - "integrations/frameworks/fiber", - "integrations/frameworks/django", - "integrations/frameworks/flask", - "integrations/frameworks/laravel", - "integrations/frameworks/rails", - "integrations/frameworks/dotnet", - "integrations/platforms/pm2" + { + "group": "View more", + "pages": [ + "integrations/frameworks/nextjs", + "integrations/frameworks/nestjs", + "integrations/frameworks/sveltekit", + "integrations/frameworks/nuxt", + "integrations/frameworks/gatsby", + "integrations/frameworks/remix", + "integrations/frameworks/vite", + "integrations/frameworks/fiber", + "integrations/frameworks/django", + "integrations/frameworks/flask", + "integrations/frameworks/laravel", + "integrations/frameworks/rails", + "integrations/frameworks/dotnet", + "integrations/platforms/pm2" + ] + } ] }, { @@ -316,7 +362,18 @@ }, { "group": "Overview", - "pages": ["sdks/overview"] + "pages": [ + "sdks/overview" + ] + }, + { + "group": "SDK's", + "pages": [ + "sdks/languages/node", + "sdks/languages/python", + "sdks/languages/java", + "sdks/languages/csharp" + ] }, { "group": "Overview", @@ -377,9 +434,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/sdks/languages/csharp.mdx b/docs/sdks/languages/csharp.mdx index ecfd67c27b..b3a1d20868 100644 --- a/docs/sdks/languages/csharp.mdx +++ b/docs/sdks/languages/csharp.mdx @@ -1,6 +1,7 @@ --- title: "Infisical .NET SDK" -icon: "C#" +sidebarTitle: ".NET" +icon: "bars" --- If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application. diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx index 40d577926c..5b8797b5d3 100644 --- a/docs/sdks/languages/java.mdx +++ b/docs/sdks/languages/java.mdx @@ -1,5 +1,6 @@ --- title: "Infisical Java SDK" +sidebarTitle: "Java" icon: "java" --- diff --git a/docs/sdks/languages/node.mdx b/docs/sdks/languages/node.mdx index faabd794cd..4816392ed6 100644 --- a/docs/sdks/languages/node.mdx +++ b/docs/sdks/languages/node.mdx @@ -1,5 +1,6 @@ --- title: "Infisical Node.js SDK" +sidebarTitle: "Node.js" icon: "node" --- @@ -42,7 +43,7 @@ app.get("/", async (req, res) => { app.listen(PORT, async () => { // initialize client - console.log(`App listening on port ${port}`); + console.log(`App listening on port ${PORT}`); }); ``` diff --git a/docs/sdks/languages/python.mdx b/docs/sdks/languages/python.mdx index da92b1a6ed..0ce221757a 100644 --- a/docs/sdks/languages/python.mdx +++ b/docs/sdks/languages/python.mdx @@ -1,5 +1,6 @@ --- title: "Infisical Python SDK" +sidebarTitle: "Python" icon: "python" --- diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx deleted file mode 100644 index c26c206483..0000000000 --- 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 3ea1762e2a..4bb56ebcd2 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 minimal 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 6013cab878..0000000000 --- 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 0000000000..262c7fb7cb --- /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 small 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) diff --git a/docs/self-hosting/configuration/schema-migrations.mdx b/docs/self-hosting/configuration/schema-migrations.mdx new file mode 100644 index 0000000000..6a94af751f --- /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 2d663790d2..0000000000 --- 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 304fffbe37..583fe16745 100644 --- a/docs/self-hosting/deployment-options/docker-compose.mdx +++ b/docs/self-hosting/deployment-options/docker-compose.mdx @@ -2,53 +2,82 @@ 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`. + +![self host sign up](images/self-hosting/applicable-to-all/selfhost-signup.png) \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 564e410f8c..ae27207cd7 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -1,163 +1,190 @@ --- title: "Kubernetes via Helm Chart" -description: "Use our Helm chart to Install Infisical on your Kubernetes cluster" +description: "Use Helm chart to install Infisical on your Kubernetes cluster" --- **Prerequisites** -- You have understanding of [Kubernetes](https://kubernetes.io/) +- You have extensive understanding of [Kubernetes](https://kubernetes.io/) - Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater - You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster -By deploying Infisical on Kubernetes, you can take advantage of its features to ensure that the application is fault-tolerant, highly available, and scalable. -To make the installation process easier and more streamlined, we have created a Helm chart that you can use to install Infisical on Kubernetes. - -Helm is a package manager for Kubernetes that simplifies the installation and management of Kubernetes applications. -With our Helm chart, you can easily install Infisical on Kubernetes, configure it to your liking, and scale it up or down as needed. - -## Install Infisical Helm repository - -```bash -helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' - -helm repo update -``` - -## Add Helm values - -Create a values.yaml file to configure various installation settings, such as the docker image tags and environment variables. To explore all configurable properties for your values file, [visit this page](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical). - -#### Set image tags - -By default, the application will use the `latest` docker image tag. This is okay for test environments; however, for production deployments it is important to pin your deployment to a particular docker image tag to prevent receiving unintended changes. - - To find the latest version number of Infisical, click [here](https://hub.docker.com/r/infisical/infisical/tags) - - -```yaml simple-values-example.yaml -backend: - replicaCount: 2 - image: - tag: "v0.39.5" # <--- update to the newest version found here https://hub.docker.com/r/infisical/infisical/tags - pullPolicy: Always -``` - -#### Configure environment variables - -You can configure environment variables for your instance of Infisical though the Helm values file under the property `backendEnvironmentVariables`. View configurable [environment variables](../configuration/envars). - -Infisical requires the following backend environment variables to be defined: _`ENCRYPTION_KEY`_, _`JWT_SIGNUP_SECRET`_, _`JWT_REFRESH_SECRET`_, _`JWT_AUTH_SECRET`_, _`JWT_MFA_SECRET`_ and _`JWT_SERVICE_SECRET`_. - - -Each of the above environment variables can be generated by running the command `openssl rand -hex 16` in your terminal. - - -However, when the above environment variables are not defined, the Helm chart -will automatically generate these environment variables for you. The generated environment variables will be saved to a Kubernetes secret and will be preserved between upgrades or uninstalls. - -```yaml simple-values-example.yaml -... -backendEnvironmentVariables: - HTTPS_ENABLED: true - INVITE_ONLY_SIGNUP: false - ... -``` - - - Infisical assumes that you have configured HTTPS. If you didn't configure HTTPS, set `HTTPS_ENABLED` to `false` in the backend environment variable to avoid frequent logouts. - - -#### Routing external traffic -By default, Infisical takes all traffic coming to your external load balancer's IP address and routes them Infisical's services. -Infisical uses Nginx to route external traffic. You can install Nginx along with Infisical by setting `ingress.enabled` to `true` in the Helm values file. View all [properties for ingress](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical). - -```yaml simple-values-example.yaml -... -ingress: - nginx: - enabled: true #<-- if you would like to install nginx along with Infisical -``` - -#### Database -Infisical uses a MongoDB as its persistence layer. With this Helm chart, a MongoDB instance is automatically spun up for use with Infisical. -When persistence is enabled, the data will be stored as Kubernetes Persistence Volume. View all [properties for mongodb](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical). - -```yaml simple-values-example.yaml -mongodb: - enabled: true - persistence: - enabled: false -``` - -To achieve high availability and data redundancy, we recommend that you use a managed document database service such as AWS Document DB, MongoDB or similar services instead of the in cluster database. -Managed database connection string can be set in the `backendEnvironmentVariables`. - -#### Example helm values -```yaml simple-values-example.yaml -backend: - replicaCount: 2 - image: - tag: "v0.39.5" - pullPolicy: Always - -backendEnvironmentVariables: - HTTPS_ENABLED: true - -ingress: - nginx: - enabled: true - -``` - - - ```yaml values.yaml - ingress: - nginx: - enabled: true - - backend: - enabled: true - name: backend - podAnnotations: {} - deploymentAnnotations: {} - replicaCount: 4 - image: - tag: "v0.39.5" - pullPolicy: IfNotPresent - kubeSecretRef: null - service: - annotations: {} - type: ClusterIP - nodePort: "" - - # View all environment variables https://infisical.com/docs/self-hosting/configuration/envars - backendEnvironmentVariables: - MONGO_URL: <> - HTTPS_ENABLED: <> + + + ```bash + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + ``` + ``` + helm repo update + ``` + + + Create a `values.yaml` file. This will be used to configure settings for the Infisical Helm chart. + To explore all configurable properties for your values file, [visit this page](https://raw.githubusercontent.com/Infisical/infisical/main/helm-charts/infisical-standalone-postgres/values.yaml). + + + By default, the Infisical version set in your helm chart will likely be outdated. + Choose the latest Infisical docker image tag from here [here](https://hub.docker.com/r/infisical/infisical/tags). - ## Mongo DB persistence - mongodb: - enabled: true - persistence: - enabled: true - ``` - + ```yaml values.yaml + infisical: + image: + repository: infisical/infisical + tag: "v0.46.2-postgres" #<-- update + pullPolicy: IfNotPresent + ``` + + Do you not use the latest docker image tag in production deployments as they can introduce unexpected changes + + -## 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. + To deploy this Helm chart, a Kubernetes secret named `infisical-secrets` must be present in the same namespace where the chart is being deployed. -```bash -## Installs to default namespace -helm install infisical-helm-charts/infisical --generate-name --values /path/to/values.yaml -``` + For a minimal installation of Infisical, you need to configure `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI`, and `REDIS_URL`. [Learn more about configuration settings](/self-hosting/configuration/envars). -## Access Infisical -Allow 3-5 minutes for the deployment to complete. Once done, you should now be able to access Infisical on the IP address exposed via Ingress on your load balancer. If you are not sure what the IP address is run `kubectl get ingress` to view the external IP address exposing Infisical. - - -Once installation is complete, you will have to create the first account. No default account is provided. - -## Related blogs -- [Set up Infisical in a development cluster](https://iamunnip.hashnode.dev/infisical-open-source-secretops-kubernetes-setup) + + + For test or proof-of-concept purposes, you may omit `DB_CONNECTION_URI` and `REDIS_URL` from `infisical-secrets`. This is because the Helm chart will automatically provision and connect to the in-cluster instances of Postgres and Redis by default. + ```yaml simple-values-example.yaml + apiVersion: v1 + kind: Secret + metadata: + name: infisical-secrets + type: Opaque + stringData: + AUTH_SECRET: <> + ENCRYPTION_KEY: <> + ``` + + + For production environments, we recommend using Cloud-based Platform as a Service (PaaS) solutions for PostgreSQL and Redis to ensure high availability. In on-premise setups, it's recommended to configure Redis and Postgres for high availability, either by using Bitnami charts or a custom configuration. + ```yaml simple-values-example.yaml + apiVersion: v1 + kind: Secret + metadata: + name: infisical-secrets + type: Opaque + stringData: + AUTH_SECRET: <> + ENCRYPTION_KEY: <> + REDIS_URL: <> + DB_CONNECTION_URI: <> + ``` + + + + + + Infisical relies a relational database, which means that database schemas need to be migrated before the instance can become operational. + + To automate this process, the chart includes a option named `infisical.autoDatabaseSchemaMigration`. + When this option is enabled, a deployment/upgrade will only occur _after_ a successful schema migration. + + + If you are using in-cluster Postgres, you may notice the migration job failing initially. + This is expected as it is waiting for the database to be in ready state. + + + + + By default, this chart uses Nginx as its Ingress controller to direct traffic to Infisical services. + + ```yaml values.yaml + ingress: + nginx: + enabled: true + ``` + + + + Once you are done configuring your `values.yaml` file, run the command below. + + ```bash + helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml + ``` + + + ```yaml values.yaml + + nameOverride: "infisical" + fullnameOverride: "infisical" + + infisical: + enabled: true + name: infisical + autoDatabaseSchemaMigration: true + fullnameOverride: "" + podAnnotations: {} + deploymentAnnotations: {} + replicaCount: 6 + + image: + repository: infisical/infisical + tag: "v0.46.2-postgres" + pullPolicy: IfNotPresent + + affinity: {} + kubeSecretRef: "infisical-secrets" + service: + annotations: {} + type: ClusterIP + nodePort: "" + + resources: + limits: + memory: 210Mi + requests: + cpu: 200m + + ingress: + enabled: true + hostName: "" + ingressClassName: nginx + nginx: + enabled: true + annotations: {} + tls: [] + + postgresql: + enabled: true + name: "postgresql" + fullnameOverride: "postgresql" + auth: + username: infisical + password: root + database: infisicalDB + + redis: + enabled: true + name: "redis" + fullnameOverride: "redis" + cluster: + enabled: false + usePassword: true + auth: + password: "mysecretpassword" + architecture: standalone + ``` + + + + + After deployment, please wait for 2-5 minutes for all pods to reach a running state. Once a significant number of pods are operational, access the IP address revealed through Ingress by your load balancer. + You can find the IP address/hostname by executing the command `kubectl get ingress`. + ![infisical-selfhost](images/self-hosting/applicable-to-all/selfhost-signup.png) + + + To upgrade your instance of Infisical simply update the docker image tag in your Halm values and rerun the command below. + + ```bash + helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml + ``` + + + Always back up your database before each upgrade, especially in a production environment. + + + + diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 5da6403373..7407925780 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -7,19 +7,32 @@ 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). + + We recommend using Cloud-based Platform as a Service (PaaS) solutions for PostgreSQL and Redis to ensure high availability. + Once you have added the required environment variables to your docker run command, execute it in your terminal to get Infisical up and running. For example: @@ -28,22 +41,22 @@ 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`. + + ![self host sign up](images/self-hosting/applicable-to-all/selfhost-signup.png) - - - 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 cbba7a9466..0000000000 --- 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 d7490f8709..0000000000 --- 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/docs/self-hosting/ee.mdx b/docs/self-hosting/ee.mdx new file mode 100644 index 0000000000..e1f3cff406 --- /dev/null +++ b/docs/self-hosting/ee.mdx @@ -0,0 +1,28 @@ +--- +title: "Using Infisical EE" +description: "How to activate Infisical Enterprise Edition (EE) features" +--- + +While most features in Infisical are free to use, others are paid and require purchasing an enterprise license to use them. + +This guide walks through how you can use these paid features in Infisical. + + + + Start by either signing up for a free demo [here](https://infisical.com/schedule-demo) or contacting team@infisical.com to purchase a license. + + Once purchased, you will be issued a license key. + + + Depending on whether or not the environment where Infisical is deployed has internet access, you may be issued a regular license or an offline license. + + - If using a regular license, you should set the value of the environment variable `LICENSE_KEY` in Infisical to the issued license key. + - If using an offline license, you should set the value of the environment variable `LICENSE_KEY_OFFLINE` in Infisical to the issued license key. + + Once your instance starts up, the license key will be validated and you’ll be able to use the paid features. + + + Once the license expires, Infisical will continue to run, but EE features will be disabled until the license is renewed or a new one is purchased. + + + \ No newline at end of file diff --git a/docs/self-hosting/faq.mdx b/docs/self-hosting/faq.mdx index 6cef86b9ca..598d408ae2 100644 --- a/docs/self-hosting/faq.mdx +++ b/docs/self-hosting/faq.mdx @@ -15,13 +15,7 @@ However, in the event you choose to use Infisical without SSL, you can do so by [Learn more about secure cookies](https://really-simple-ssl.com/definition/what-are-secure-cookies/) - - Infisical leverages the robust container orchestration capabilities of Kubernetes and the inherent high availability features of Bitnami MongoDB to ensure resilience and fault tolerance. - By deploying multiple replicas of Infisical application on Kubernetes, operations can continue even if a single instance fails. - - Additionally, Bitnami MongoDB supports replica sets, which provide data redundancy and automatic failover for the underlying database. - Kubernetes Services facilitate load balancing, effectively distributing traffic across your application's instances and ensuring optimal performance. - The combination of Kubernetes' self-healing mechanisms and Bitnami MongoDB's failover capabilities work together to create a highly available and fault-tolerant application capable of recovering gracefully from unexpected failures. - - To further increase data redundancy, we recommend that you use a managed MongoDB service for your self hosted instance of Infisical. + + Follow the step by step guide [here](self-hosting/guides/mongo-to-postgres) to learn how. + diff --git a/docs/self-hosting/guides/mongo-to-postgres.mdx b/docs/self-hosting/guides/mongo-to-postgres.mdx new file mode 100644 index 0000000000..f8a6cca0fe --- /dev/null +++ b/docs/self-hosting/guides/mongo-to-postgres.mdx @@ -0,0 +1,201 @@ +--- +title: "Migrate Mongo to Postgres" +description: "How to migrate from MongoDB to PostgreSQL for Infisical" +--- + +This guide will provide step by step instructions on migrating your Infisical instance running on MongoDB to the newly released PostgreSQL version of Infisical. +The newly released Postgres version of Infisical is the only version of Infisical that will receive feature updates and patches going forward. + + + If you have a small set of secrets, we recommend you to download the secrets and upload them to your new instance of Infisical instead of running the migration script. + + +## Prerequisites + +Before starting the migration, ensure you have the following command line tools installed: + +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [pg_dump](https://www.postgresql.org/docs/current/app-pgrestore.html) +- [pg_restore](https://www.postgresql.org/docs/current/app-pgdump.html) +- [mongodump](https://www.mongodb.com/docs/database-tools/mongodump/) +- [mongorestore](https://www.mongodb.com/docs/database-tools/mongorestore/) +- [Docker](https://docs.docker.com/engine/install/) + +## Prepare for migration + + + + While the migration script will not mutate any MongoDB production data, we recommend you to take a backup of your MongoDB instance if possible. + + + To prevent new data entries during the migration, set your Infisical instance to migration mode by setting the environment variable `MIGRATION_MODE=true` and redeploying your instance. + This mode will block all write operations, only allowing GET requests. It also disables user logins and sets up a migration page to prevent UI interactions. + ![migration mode](/images/self-hosting/guides/mongo-postgres/mongo-migration.png) + + + Start local instances of MongoDB and Postgres. This will be used in later steps to process and transform the data locally. + + To start local instances of the two databases, create a file called `docker-compose.yaml` as shown below. + + ```yaml docker-compose.yaml + version: '3.1' + + services: + mongodb: + image: mongo + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + + postgres: + image: postgres + restart: always + environment: + POSTGRES_PASSWORD: example + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + volumes: + mongodb_data: + postgres_data: + ``` + + Next, run the command below in the same working directory where the `docker-compose.yaml` file resides to start both services. + + ``` + docker-compose up + ``` + + + + +## Dump MongoDB +To speed up the data transformation process, the first step involves transferring the production data from Infisical's MongoDB to a local machine. +This is achieved by creating a dump of the production database and then uploading this dumped data into a local Mongo instance. +By having a running local instance of the production database, we will significantly reduce the time it takes to run the migration script. + + + + + ``` + mongodump --uri= --archive="mongodump-db" --db= --excludeCollection=auditlogs + ``` + + + + ``` + mongorestore --uri=mongodb://root:example@localhost:27017/ --archive="mongodump-db" + ``` + + + +## Start the migration + +Once started, the migration script will transform MongoDB data into an equivalent PostgreSQL format. + + + + Clone the Infisical MongoDB repository. + ``` + git clone -b infisical/v0.46.11-postgres https://github.com/Infisical/infisical.git + ``` + + + ``` + cd backend + ``` + + ``` + npm install + ``` + + + ``` + cd pg-migrator + ``` + + ``` + npm install + ``` + + + ``` + npm run migration + ``` + + When executing the above command, you'll be asked to provide the MongoDB connection string for the database containing your production Infisical data. Since our production Mongo data is transferred to a local Mongo instance, you should input the connection string for this local instance. + + ``` + mongodb://root:example@localhost:27017/?authSource=admin + ``` + + + Remember to replace `` with the name of the MongoDB database. If you are not sure the name, you can use [Compass](https://www.mongodb.com/products/tools/compass) to view the available databases. + + + + Next, you will be asked to enter the Postgres connection string for the database where the transformed data should be stored. + Input the connection string of the local Postgres instance that was set up earlier in the guide. + + ``` + postgres://infisical:infisical@localhost/infisical?sslmode=disable + ``` + + + + Once the script has completed, you will notice a new folder has been created called `db` in the `pg-migrator` folder. + This folder contains meta data for schema mapping and can be helpful when debugging migration related issues. + We highly recommend you to make a copy of this folder in case you need assistance from the Infisical team during your migration process. + + + The `db` folder does not contain any sensitive data + + + + +## Finalizing Migration +At this stage, the data from the Mongo instance of Infisical should have been successfully converted into its Postgres equivalent. +The remaining step involves transferring the local Postgres database, which now contains all the migrated data, to your chosen production Postgres environment. +Rather than transferring the data row-by-row from your local machine to the production Postgres database, we will first create a dump file from the local Postgres and then upload this file to your production Postgres instance. + + + + ``` + pg_dump -h localhost -U infisical -Fc -b -v -f dumpfilelocation.sql -d infisical + ``` + + + ``` + pg_restore --clean -v -h -U -d -j 2 dumpfilelocation.sql + ``` + + + Remember to replace ``, ``, `` with the corresponding details of your production Postgres database. + + + + Use a tool like Beekeeper Studio to confirm that the data has been successfully transferred to your production Postgres DB. + + + +## Post-Migration Steps + +Once the data migration to PostgreSQL is complete, you're ready to deploy Infisical using the deployment method of your choice. +For guidance on deployment options, please visit the [self-hosting documentation](/self-hosting/overview). +Remember to transfer the necessary [environment variables](/self-hosting/configuration/envars) from the MongoDB version of Infisical to the new Postgres based Infisical; rest assured, they are fully compatible. + + +The first deployment of Postgres based Infisical must be deployed with Docker image tag `v0.46.11-postgres`. +After deploying this version, you can proceed to update to any subsequent versions. + + +## Additional discussion +- When you visit Infisical's [docker hub](https://hub.docker.com/r/infisical/infisical) page, you will notice that image tags end with `-postgres`. +This is to indicate that this version of Infisical runs on the new Postgres backend. Any image tag that does not end in `postgres` runs on MongoDB. \ No newline at end of file diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index f5e754b1fc..f8089719d6 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -14,13 +14,6 @@ Choose from a variety of deployment options listed below to get started. Use the fully packaged docker image to deploy Infisical anywhere - - Automatically create and deploy Infisical on to a Kubernetes cluster - Use our Helm chart to Install Infisical on your Kubernetes cluster - - Install infisical with just a few clicks using our Cloud Formation template - - - Deploy Infisical with AWS Lightsail - - - Deploy Infisical with GCP Cloud Run - - - Deploy Infisical with Azure App Services - - - Deploy Infisical with Azure Container Instances - - - Deploy Infisical with Fly.io - - - Deploy Infisical with Railway - diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f28920f064..5e30edfc3e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popper": "^1.1.3", "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", @@ -48,7 +49,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 +69,7 @@ "next": "^12.3.4", "nprogress": "^0.2.0", "picomatch": "^2.3.1", - "posthog-js": "^1.58.0", + "posthog-js": "^1.105.6", "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", @@ -82,9 +83,9 @@ "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", - "sanitize-html": "^2.11.0", + "sanitize-html": "^2.12.1", "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 +95,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 +2488,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 +3253,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", @@ -4783,6 +5224,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", @@ -6574,6 +7047,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 +9435,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 +9737,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 +9787,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 +9871,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 +9882,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 +10161,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 +10889,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 +11479,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 +11532,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 +12174,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 +13249,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 +13391,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 +13978,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 +14230,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 +14957,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 +17402,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 +17506,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 +17556,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 +17734,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 +17746,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 +17755,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 +17772,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 +17784,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 +19098,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.105.6", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.105.6.tgz", + "integrity": "sha512-5ITXsh29XIuNohHLy21nawGnfFZDpyt+yfnWge9sJl5yv0nNuoUmLiDgw1tJafoqGrfd5CUasKyzSI21HxsSeQ==", "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 +19144,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 +19165,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 +19177,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 +19480,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 +19719,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 +19804,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 +19818,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" } @@ -20570,9 +21144,9 @@ "dev": true }, "node_modules/sanitize-html": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", - "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", + "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -20820,25 +21394,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 +21507,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 +21527,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 +21911,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 +22383,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 +22924,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 +24139,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 +24150,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 53e383f426..6a1666f056 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popper": "^1.1.3", "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", @@ -56,7 +57,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 +77,7 @@ "next": "^12.3.4", "nprogress": "^0.2.0", "picomatch": "^2.3.1", - "posthog-js": "^1.58.0", + "posthog-js": "^1.105.6", "query-string": "^7.1.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.1", @@ -90,9 +91,9 @@ "react-markdown": "^8.0.3", "react-redux": "^8.0.2", "react-table": "^7.8.0", - "sanitize-html": "^2.11.0", + "sanitize-html": "^2.12.1", "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 +103,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/public/images/secretRotation/aws-iam.svg b/frontend/public/images/secretRotation/aws-iam.svg new file mode 100644 index 0000000000..0b3b2387bd --- /dev/null +++ b/frontend/public/images/secretRotation/aws-iam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx deleted file mode 100644 index 9f109b931f..0000000000 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { Select, SelectItem } from "@app/components/v2"; -import { useSubscription, useWorkspace } from "@app/context"; -import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission"; -import { - useDeleteUserFromWorkspace, - useGetUserWsKey, - useUpdateUserWorkspaceRole, - useUploadWsKey -} from "@app/hooks/api"; - -import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto"; -import guidGenerator from "../../utilities/randomId"; -import Button from "../buttons/Button"; -import UpgradePlanModal from "../dialog/UpgradePlan"; - -// const roles = ['admin', 'user']; -// TODO: Set type for this -type Props = { - userData: any[]; - changeData: (users: any[]) => void; - myUser: string; - filter: string; - isUserListLoading: boolean; -}; - -type EnvironmentProps = { - name: string; - slug: string; -}; - -/** - * This is the component that shows the users of a certin project - * #TODO: add the possibility of choosing and doing operations on multiple users. - * @param {*} props - * @returns - */ -const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => { - const { currentWorkspace } = useWorkspace(); - const { subscription } = useSubscription(); - const { data: wsKey } = useGetUserWsKey(currentWorkspace?.id ?? ""); - - const { mutateAsync: deleteUserFromWorkspaceMutateAsync } = useDeleteUserFromWorkspace(); - const { mutateAsync: uploadWsKeyMutateAsync } = useUploadWsKey(); - const { mutateAsync: updateUserWorkspaceRoleMutateAsync } = useUpdateUserWorkspaceRole(); - // const [roleSelected, setRoleSelected] = useState( - // Array(userData?.length).fill(userData.map((user) => user.role)) - // ); - const router = useRouter(); - const [myRole, setMyRole] = useState("member"); - const [workspaceEnvs, setWorkspaceEnvs] = useState([]); - const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false); - const { createNotification } = useNotificationContext(); - - 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 handleRoleUpdate = async (index: number, e: string) => { - await updateUserWorkspaceRoleMutateAsync({ - workspaceId, - membershipId: userData[index].membershipId, - role: e.toLowerCase() - }); - createNotification({ - text: "Successfully changed user role.", - type: "success" - }); - }; - - const handlePermissionUpdate = ( - index: number, - val: string, - membershipId: string, - slug: string - ) => { - let denials: { ability: string; environmentSlug: string }[]; - if (val === "Read Only") { - denials = [ - { - ability: "write", - environmentSlug: slug - } - ]; - } else if (val === "No Access") { - denials = [ - { - ability: "write", - environmentSlug: slug - }, - { - ability: "read", - environmentSlug: slug - } - ]; - } else if (val === "Add Only") { - denials = [ - { - ability: "read", - environmentSlug: slug - } - ]; - } else { - denials = []; - } - - if (subscription?.rbac === false) { - setIsUpgradeModalOpen(true); - } else { - const allDenials = userData[index].deniedPermissions - .filter( - (perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug - ) - .concat(denials); - updateUserProjectPermission({ membershipId, denials: allDenials }); - changeData([ - ...userData.slice(0, index), - ...[ - { - key: userData[index].key, - firstName: userData[index].firstName, - lastName: userData[index].lastName, - email: userData[index].email, - role: userData[index].role, - status: userData[index].status, - userId: userData[index].userId, - membershipId: userData[index].membershipId, - publicKey: userData[index].publicKey, - deniedPermissions: allDenials - } - ], - ...userData.slice(index + 1, userData?.length) - ]); - createNotification({ - text: "Successfully changed user permissions.", - type: "success" - }); - } - }; - - useEffect(() => { - setMyRole(userData.filter((user) => user.email === myUser)[0]?.role); - (async () => { - if (currentWorkspace) { - setWorkspaceEnvs(currentWorkspace.environments); - } - })(); - }, [userData, myUser, currentWorkspace]); - - const grantAccess = async (id: string, publicKey: string) => { - if (wsKey) { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: wsKey.encryptedKey, - nonce: wsKey.nonce, - publicKey: wsKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKeyMutateAsync({ - workspaceId, - userId: id, - encryptedKey: ciphertext, - nonce - }); - router.reload(); - } - }; - - const closeUpgradeModal = () => { - setIsUpgradeModalOpen(false); - }; - - return ( -
-
- {subscription && ( - - )} - - - - - - - {workspaceEnvs.map((env) => ( - - ))} - - - - {!isUserListLoading && - userData?.filter( - (user) => - user.firstName?.toLowerCase().includes(filter) || - user.lastName?.toLowerCase().includes(filter) || - user.email?.toLowerCase().includes(filter) - ).length > 0 && - userData - ?.filter( - (user) => - user.firstName?.toLowerCase().includes(filter) || - user.lastName?.toLowerCase().includes(filter) || - user.email?.toLowerCase().includes(filter) - ) - .map((row, index) => ( - - - - - {workspaceEnvs.map((env) => ( - - ))} - - - ))} - {isUserListLoading && ( - <> - - - - )} - -
NAMEEMAILROLE - - {env.slug.toUpperCase()} -
-
- {/* PERMISSION */} -
-
- {row.firstName} {row.lastName} - - {row.email} - -
- - {row.status === "completed" && myUser !== row.email && ( -
-
- )} -
-
- - - {myUser !== row.email && - // row.role !== "admin" && - myRole !== "member" ? ( -
-
- ) : ( -
- )} -
-
- ); -}; - -export default ProjectUsersTable; diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 2bf5969154..92a6875a67 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -56,18 +56,20 @@ export default function NavHeader({ return (
-
+
{currentOrg?.name?.charAt(0)}
- + {currentOrg?.name} {isProjectRelated && ( <> -
{currentWorkspace?.name}
+
+ {currentWorkspace?.name} +
)} {isOrganizationRelated && ( diff --git a/frontend/src/components/signup/EnterEmailStep.tsx b/frontend/src/components/signup/EnterEmailStep.tsx index e317a4ea5b..fa26e80b94 100644 --- a/frontend/src/components/signup/EnterEmailStep.tsx +++ b/frontend/src/components/signup/EnterEmailStep.tsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; +import axios from "axios"; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useSendVerificationEmail } from "@app/hooks/api"; import { Button, Input } from "../v2"; @@ -25,7 +27,8 @@ export default function EnterEmailStep({ setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element { - const { mutateAsync } = useSendVerificationEmail(); + const { createNotification } = useNotificationContext(); + const { mutateAsync, isLoading } = useSendVerificationEmail(); const [emailError, setEmailError] = useState(false); const { t } = useTranslation(); @@ -46,8 +49,18 @@ export default function EnterEmailStep({ // If everything is correct, go to the next step if (!emailCheckBool) { - await mutateAsync({ email }); - incrementStep(); + try { + await mutateAsync({ email }); + incrementStep(); + } catch(e) { + if (axios.isAxiosError(e)) { + const { message = "Something went wrong" } = e.response?.data as { message: string}; + createNotification({ + type: "error", + text: message + }) + } + } } }; @@ -78,6 +91,8 @@ export default function EnterEmailStep({ className='h-14' colorSchema="primary" variant="outline_bg" + isLoading={isLoading} + isDisabled={isLoading} > {String(t("signup.step1-submit"))}
diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 7af9ccc8fd..994ad5a724 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -245,6 +245,7 @@ export default function UserInfoStep({ placeholder="Infisical" onChange={(e) => setOrganizationName(e.target.value)} value={organizationName} + maxLength={64} isRequired className="h-12" /> diff --git a/frontend/src/components/utilities/cryptography/crypto.ts b/frontend/src/components/utilities/cryptography/crypto.ts index 4e2d981a7d..c0e5d4c218 100644 --- a/frontend/src/components/utilities/cryptography/crypto.ts +++ b/frontend/src/components/utilities/cryptography/crypto.ts @@ -13,12 +13,12 @@ nacl.util = require("tweetnacl-util"); */ const generateKeyPair = () => { const pair = nacl.box.keyPair(); - - return ({ - publicKey: nacl.util.encodeBase64(pair.publicKey), - privateKey: nacl.util.encodeBase64(pair.secretKey) - }); -} + + return { + publicKey: nacl.util.encodeBase64(pair.publicKey), + privateKey: nacl.util.encodeBase64(pair.secretKey) + }; +}; type EncryptAsymmetricProps = { plaintext: string; @@ -29,27 +29,19 @@ type EncryptAsymmetricProps = { /** * Verify that private key [privateKey] is the one that corresponds to * the public key [publicKey] - * @param {Object} + * @param {Object} * @param {String} - base64-encoded Nacl private key * @param {String} - base64-encoded Nacl public key */ -const verifyPrivateKey = ({ - privateKey, - publicKey -}: { - privateKey: string; - publicKey: string; -}) => { +const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => { const derivedPublicKey = nacl.util.encodeBase64( - nacl.box.keyPair.fromSecretKey( - nacl.util.decodeBase64(privateKey) - ).publicKey + nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey ); - + if (derivedPublicKey !== publicKey) { throw new Error("Failed to verify private key"); } -} +}; /** * Derive a key from password [password] and salt [salt] using Argon2id @@ -218,7 +210,14 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): try { plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key }); } catch (err) { - console.log("Failed to perform decryption"); + console.log("Failed to decrypt with the following parameters", { + ciphertext, + iv, + tag, + key + }); + console.log("Failed to perform decryption", err); + process.exit(1); } @@ -229,7 +228,8 @@ export { decryptAssymmetric, decryptSymmetric, deriveArgonKey, - encryptAssymmetric, + encryptAssymmetric, encryptSymmetric, generateKeyPair, - verifyPrivateKey}; + verifyPrivateKey +}; diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts index 610e2a6f16..943502cdfb 100644 --- a/frontend/src/components/utilities/secrets/encryptSecrets.ts +++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts @@ -56,7 +56,6 @@ const encryptSecrets = async ({ publicKey: wsKey.sender.publicKey, privateKey: PRIVATE_KEY }); - } else { // case: a (shared) key does not exist for the workspace randomBytes = crypto.randomBytes(16).toString("hex"); @@ -116,7 +115,6 @@ const encryptSecrets = async ({ return result; }); - } catch (error) { console.log("Error while encrypting secrets"); } diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx index 87e2781190..dc0e1a4914 100644 --- a/frontend/src/components/v2/Checkbox/Checkbox.tsx +++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx @@ -30,7 +30,7 @@ export const Checkbox = ({
-