diff --git a/.env.example b/.env.example index 67110d69a..a6f134ace 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,6 @@ CLIENT_SECRET_GITLAB_LOGIN= CAPTCHA_SECRET= NEXT_PUBLIC_CAPTCHA_SITE_KEY= + +PLAIN_API_KEY= +PLAIN_WISH_LABEL_IDS= diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml new file mode 100644 index 000000000..df811d565 --- /dev/null +++ b/.github/workflows/build-binaries.yml @@ -0,0 +1,99 @@ +name: Build Binaries and Deploy + +on: + workflow_dispatch: + inputs: + version: + description: "Version number" + required: true + type: string + +defaults: + run: + working-directory: ./backend + +jobs: + build-and-deploy: + runs-on: ubuntu-20.04 + strategy: + matrix: + arch: [x64, arm64] + os: [linux, win] + include: + - os: linux + target: node20-linux + - os: win + target: node20-win + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install pkg + run: npm install -g @yao-pkg/pkg + + - name: Install dependencies (backend) + run: npm install + + - name: Install dependencies (frontend) + run: npm install --prefix ../frontend + + - name: Prerequisites for pkg + run: npm run binary:build + + - name: Package into node binary + run: | + if [ "${{ matrix.os }}" != "linux" ]; then + pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} . + else + pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core . + fi + + # Set up .deb package structure (Debian/Ubuntu only) + - name: Set up .deb package structure + if: matrix.os == 'linux' + run: | + mkdir -p infisical-core/DEBIAN + mkdir -p infisical-core/usr/local/bin + cp ./binary/infisical-core infisical-core/usr/local/bin/ + chmod +x infisical-core/usr/local/bin/infisical-core + + - name: Create control file + if: matrix.os == 'linux' + run: | + cat < infisical-core/DEBIAN/control + Package: infisical-core + Version: ${{ github.event.inputs.version }} + Section: base + Priority: optional + Architecture: ${{ matrix.arch == 'x64' && 'amd64' || matrix.arch }} + Maintainer: Infisical + Description: Infisical Core standalone executable (app.infisical.com) + EOF + + # Build .deb file (Debian/Ubunutu only) + - name: Build .deb package + if: matrix.os == 'linux' + run: | + dpkg-deb --build infisical-core + mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb + + - uses: actions/setup-python@v4 + - run: pip install --upgrade cloudsmith-cli + + # Publish .deb file to Cloudsmith (Debian/Ubuntu only) + - name: Publish to Cloudsmith (Debian/Ubuntu) + if: matrix.os == 'linux' + working-directory: ./backend + run: cloudsmith push deb --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.deb + + # Publish .exe file to Cloudsmith (Windows only) + - name: Publish to Cloudsmith (Windows) + if: matrix.os == 'win' + working-directory: ./backend + run: cloudsmith push raw infisical/infisical-core ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }}.exe --republish --no-wait-for-sync --version ${{ github.event.inputs.version }} --api-key ${{ secrets.CLOUDSMITH_API_KEY }} diff --git a/.github/workflows/build-staging-and-deploy-aws.yml b/.github/workflows/build-staging-and-deploy-aws.yml index a9b2046ae..5c699c9d4 100644 --- a/.github/workflows/build-staging-and-deploy-aws.yml +++ b/.github/workflows/build-staging-and-deploy-aws.yml @@ -50,6 +50,13 @@ jobs: environment: name: Gamma steps: + - uses: twingate/github-action@v1 + with: + # The Twingate Service Key used to connect Twingate to the proper service + # Learn more about [Twingate Services](https://docs.twingate.com/docs/services) + # + # Required + service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - name: Checkout code uses: actions/checkout@v2 - name: Setup Node.js environment @@ -74,21 +81,21 @@ jobs: uses: pr-mpt/actions-commit-hash@v2 - name: Download task definition run: | - aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json + aws ecs describe-task-definition --task-definition infisical-core-gamma-stage --query taskDefinition > task-definition.json - name: Render Amazon ECS task definition id: render-web-container uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: task-definition.json - container-name: infisical-core-platform + container-name: infisical-core image: infisical/staging_infisical:${{ steps.commit.outputs.short }} environment-variables: "LOG_LEVEL=info" - name: Deploy to Amazon ECS service uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.render-web-container.outputs.task-definition }} - service: infisical-core-platform - cluster: infisical-core-platform + service: infisical-core-gamma-stage + cluster: infisical-gamma-stage wait-for-service-stability: true production-postgres-deployment: @@ -98,6 +105,13 @@ jobs: environment: name: Production steps: + - uses: twingate/github-action@v1 + with: + # The Twingate Service Key used to connect Twingate to the proper service + # Learn more about [Twingate Services](https://docs.twingate.com/docs/services) + # + # Required + service-key: ${{ secrets.TWINGATE_SERVICE_KEY }} - name: Checkout code uses: actions/checkout@v2 - name: Setup Node.js environment diff --git a/.github/workflows/check-migration-file-edited.yml b/.github/workflows/check-migration-file-edited.yml new file mode 100644 index 000000000..e94a573c6 --- /dev/null +++ b/.github/workflows/check-migration-file-edited.yml @@ -0,0 +1,25 @@ +name: Check migration file edited + +on: + pull_request: + types: [opened, synchronize] + paths: + - 'backend/src/db/migrations/**' + +jobs: + rename: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check any migration files are modified, renamed or duplicated. + run: | + git diff --name-status HEAD^ HEAD backend/src/db/migrations | grep '^M\|^R\|^C' || true | cut -f2 | xargs -r -n1 basename > edited_files.txt + if [ -s edited_files.txt ]; then + echo "Exiting migration files cannot be modified." + cat edited_files.txt + exit 1 + fi diff --git a/.github/workflows/update-be-new-migration-latest-timestamp.yml b/.github/workflows/update-be-new-migration-latest-timestamp.yml deleted file mode 100644 index 684c78654..000000000 --- a/.github/workflows/update-be-new-migration-latest-timestamp.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Rename Migrations - -on: - pull_request: - types: [closed] - paths: - - 'backend/src/db/migrations/**' - -jobs: - rename: - runs-on: ubuntu-latest - if: github.event.pull_request.merged == true - - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get list of newly added files in migration folder - run: | - git diff --name-status HEAD^ HEAD backend/src/db/migrations | grep '^A' | cut -f2 | xargs -n1 basename > added_files.txt - if [ ! -s added_files.txt ]; then - echo "No new files added. Skipping" - echo "SKIP_RENAME=true" >> $GITHUB_ENV - fi - - - name: Script to rename migrations - if: env.SKIP_RENAME != 'true' - run: python .github/resources/rename_migration_files.py - - - name: Commit and push changes - if: env.SKIP_RENAME != 'true' - run: | - git config user.name github-actions - git config user.email github-actions@github.com - git add ./backend/src/db/migrations - rm added_files.txt - git commit -m "chore: renamed new migration files to latest timestamp (gh-action)" - - - name: Get PR details - id: pr_details - run: | - PR_NUMBER=${{ github.event.pull_request.number }} - PR_MERGER=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | jq -r '.merged_by.login') - - echo "PR Number: $PR_NUMBER" - echo "PR Merger: $PR_MERGER" - echo "pr_merger=$PR_MERGER" >> $GITHUB_OUTPUT - - - name: Create Pull Request - if: env.SKIP_RENAME != 'true' - uses: peter-evans/create-pull-request@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: 'chore: renamed new migration files to latest UTC (gh-action)' - title: 'GH Action: rename new migration file timestamp' - branch-suffix: timestamp - reviewers: ${{ steps.pr_details.outputs.pr_merger }} diff --git a/.gitignore b/.gitignore index b04860071..fd008a493 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ frontend-build *.tgz cli/infisical-merge cli/test/infisical-merge +/backend/binary diff --git a/.infisicalignore b/.infisicalignore index 855047fe4..b7fc38b35 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -5,3 +5,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/M frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292 docs/self-hosting/configuration/envars.mdx:generic-api-key:106 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451 +docs/mint.json:generic-api-key:651 diff --git a/backend/babel.config.json b/backend/babel.config.json new file mode 100644 index 000000000..59480d084 --- /dev/null +++ b/backend/babel.config.json @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react"], + "plugins": ["@babel/plugin-syntax-import-attributes", "babel-plugin-transform-import-meta"] +} diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 09ab05443..cc1f9afc2 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -3,7 +3,6 @@ import "ts-node/register"; import dotenv from "dotenv"; import jwt from "jsonwebtoken"; -import knex from "knex"; import path from "path"; import { seedData1 } from "@app/db/seed-data"; @@ -15,6 +14,7 @@ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { mockQueue } from "./mocks/queue"; import { mockSmtpServer } from "./mocks/smtp"; import { mockKeyStore } from "./mocks/keystore"; +import { initDbConnection } from "@app/db"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { @@ -23,23 +23,21 @@ export default { async setup() { const logger = await initLogger(); const cfg = initEnvConfig(logger); - const db = knex({ - client: "pg", - connection: cfg.DB_CONNECTION_URI, - migrations: { - directory: path.join(__dirname, "../src/db/migrations"), - extension: "ts", - tableName: "infisical_migrations" - }, - seeds: { - directory: path.join(__dirname, "../src/db/seeds"), - extension: "ts" - } + const db = initDbConnection({ + dbConnectionUri: cfg.DB_CONNECTION_URI, + dbRootCert: cfg.DB_ROOT_CERT }); try { - await db.migrate.latest(); - await db.seed.run(); + await db.migrate.latest({ + directory: path.join(__dirname, "../src/db/migrations"), + extension: "ts", + tableName: "infisical_migrations" + }); + await db.seed.run({ + directory: path.join(__dirname, "../src/db/seeds"), + extension: "ts" + }); const smtp = mockSmtpServer(); const queue = mockQueue(); const keyStore = mockKeyStore(); @@ -74,7 +72,14 @@ export default { // @ts-expect-error type delete globalThis.jwtToken; // called after all tests with this env have been run - await db.migrate.rollback({}, true); + await db.migrate.rollback( + { + directory: path.join(__dirname, "../src/db/migrations"), + extension: "ts", + tableName: "infisical_migrations" + }, + true + ); await db.destroy(); } }; diff --git a/backend/package-lock.json b/backend/package-lock.json index 8bd4a98a0..ad341d15c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-iam": "^3.525.0", "@aws-sdk/client-secrets-manager": "^3.504.0", + "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", @@ -28,7 +29,8 @@ "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.10.0", "@serdnam/pino-cloudwatch-transport": "^1.0.4", - "@sindresorhus/slugify": "^2.2.1", + "@sindresorhus/slugify": "1.1.0", + "@team-plain/typescript-sdk": "^4.6.1", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -38,6 +40,7 @@ "bcrypt": "^5.1.1", "bullmq": "^5.4.2", "cassandra-driver": "^4.7.2", + "connect-redis": "^7.1.1", "cron": "^3.1.7", "dotenv": "^16.4.1", "fastify": "^4.26.0", @@ -55,8 +58,9 @@ "lodash.isequal": "^4.5.0", "ms": "^2.1.3", "mysql2": "^3.9.8", - "nanoid": "^5.0.4", + "nanoid": "^3.3.4", "nodemailer": "^6.9.9", + "openid-client": "^5.6.5", "ora": "^7.0.1", "oracledb": "^6.4.0", "passport-github": "^1.1.0", @@ -70,13 +74,22 @@ "posthog-node": "^3.6.2", "probot": "^13.0.0", "smee-client": "^2.0.0", + "tedious": "^18.2.1", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "uuid": "^9.0.1", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.4" }, + "bin": { + "backend": "dist/main.js" + }, "devDependencies": { + "@babel/cli": "^7.18.10", + "@babel/core": "^7.18.10", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/preset-env": "^7.18.10", + "@babel/preset-react": "^7.24.7", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "^9.0.5", @@ -94,6 +107,8 @@ "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", + "@yao-pkg/pkg": "^5.12.0", + "babel-plugin-transform-import-meta": "^2.2.1", "eslint": "^8.56.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -106,7 +121,7 @@ "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", "rimraf": "^5.0.5", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", "tsconfig-paths": "^4.2.0", "tsup": "^8.0.1", @@ -125,6 +140,29 @@ "node": ">=0.10.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@aws-crypto/crc32": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", @@ -1423,6 +1461,1066 @@ "@aws-sdk/credential-provider-node": "^3.504.0" } }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.609.0.tgz", + "integrity": "sha512-A0B3sDKFoFlGo8RYRjDBWHXpbgirer2bZBkCIzhSPHc1vOFHt/m2NcUoE2xnBKXJFrptL1xDkvo1P+XYp/BfcQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.609.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/credential-provider-node": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.609.0.tgz", + "integrity": "sha512-gqXGFDkIpKHCKAbeJK4aIDt3tiwJ26Rf5Tqw9JS6BYXsdMeOB8FTzqD9R+Yc1epHd8s5L94sdqXT5PapgxFZrg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.609.0.tgz", + "integrity": "sha512-0bNPAyPdkWkS9EGB2A9BZDkBNrnVCBzk5lYRezoT4K3/gi9w1DTYH5tuRdwaTZdxW19U1mq7CV0YJJARKO1L9Q==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/credential-provider-node": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/core": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.609.0.tgz", + "integrity": "sha512-ptqw+DTxLr01+pKjDUuo53SEDzI+7nFM3WfQaEo0yhDg8vWw8PER4sWj1Ysx67ksctnZesPUjqxd5SHbtdBxiA==", + "dependencies": { + "@smithy/core": "^2.2.4", + "@smithy/protocol-http": "^4.0.3", + "@smithy/signature-v4": "^3.1.2", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.609.0.tgz", + "integrity": "sha512-v69ZCWcec2iuV9vLVJMa6fAb5xwkzN4jYIT8yjo2c4Ia/j976Q+TPf35Pnz5My48Xr94EFcaBazrWedF+kwfuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.609.0.tgz", + "integrity": "sha512-GQQfB9Mk4XUZwaPsk4V3w8MqleS6ApkZKVQn3vTLAKa8Y7B2Imcpe5zWbKYjDd8MPpMWjHcBGFTVlDRFP4zwSQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.609.0.tgz", + "integrity": "sha512-hwaBfXuBTv6/eAdEsDfGcteYUW6Km7lvvubbxEdxIuJNF3vswR7RMGIXaEC37hhPkTTgd3H0TONammhwZIfkog==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.609.0", + "@aws-sdk/credential-provider-http": "3.609.0", + "@aws-sdk/credential-provider-process": "3.609.0", + "@aws-sdk/credential-provider-sso": "3.609.0", + "@aws-sdk/credential-provider-web-identity": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.609.0.tgz", + "integrity": "sha512-4J8/JRuqfxJDGD9jTHVCBxCvYt7/Vgj2Stlhj930mrjFPO/yRw8ilAAZxBWe0JHPX3QwepCmh4ErZe53F5ysxQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.609.0", + "@aws-sdk/credential-provider-http": "3.609.0", + "@aws-sdk/credential-provider-ini": "3.609.0", + "@aws-sdk/credential-provider-process": "3.609.0", + "@aws-sdk/credential-provider-sso": "3.609.0", + "@aws-sdk/credential-provider-web-identity": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.609.0.tgz", + "integrity": "sha512-Ux35nGOSJKZWUIM3Ny0ROZ8cqPRUEkh+tR3X2o9ydEbFiLq3eMMyEnHJqx4EeUjLRchidlm4CCid9GxMe5/gdw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.609.0.tgz", + "integrity": "sha512-oQPGDKMMIxjvTcm86g07RPYeC7mCNk+29dPpY15ZAPRpAF7F0tircsC3wT9fHzNaKShEyK5LuI5Kg/uxsdy+Iw==", + "dependencies": { + "@aws-sdk/client-sso": "3.609.0", + "@aws-sdk/token-providers": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.609.0.tgz", + "integrity": "sha512-U+PG8NhlYYF45zbr1km3ROtBMYqyyj/oK8NRp++UHHeuavgrP+4wJ4wQnlEaKvJBjevfo3+dlIBcaeQ7NYejWg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.609.0.tgz", + "integrity": "sha512-iTKfo158lc4jLDfYeZmYMIBHsn8m6zX+XB6birCSNZ/rrlzAkPbGE43CNdKfvjyWdqgLMRXF+B+OcZRvqhMXPQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.609.0.tgz", + "integrity": "sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.609.0.tgz", + "integrity": "sha512-6sewsYB7/o/nbUfA99Aa/LokM+a/u4Wpm/X2o0RxOsDtSB795ObebLJe2BxY5UssbGaWkn7LswyfvrdZNXNj1w==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.609.0.tgz", + "integrity": "sha512-nbq7MXRmeXm4IDqh+sJRAxGPAq0OfGmGIwKvJcw66hLoG8CmhhVMZmIAEBDFr57S+YajGwnLLRt+eMI05MMeVA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.609.0.tgz", + "integrity": "sha512-lMHBG8zg9GWYBc9/XVPKyuAUd7iKqfPP7z04zGta2kGNOKbUTeqmAdc1gJGku75p4kglIPlGBorOxti8DhRmKw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/token-providers": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.609.0.tgz", + "integrity": "sha512-WvhW/7XSf+H7YmtiIigQxfDVZVZI7mbKikQ09YpzN7FeN3TmYib1+0tB+EE9TbICkwssjiFc71FEBEh4K9grKQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", + "integrity": "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-endpoints": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.609.0.tgz", + "integrity": "sha512-Rh+3V8dOvEeE1aQmUy904DYWtLUEJ7Vf5XBPlQ6At3pBhp+zpXbsnpZzVL33c8lW1xfj6YPwtO6gOeEsl1juCQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.609.0.tgz", + "integrity": "sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.609.0.tgz", + "integrity": "sha512-DlZBwQ/HkZyf3pOWc7+wjJRk5R7x9YxHhs2szHwtv1IW30KMabjjjX0GMlGJ9LLkBHkbaaEY/w9Tkj12XRLhRg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", + "integrity": "sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/config-resolver": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.4.tgz", + "integrity": "sha512-VwiOk7TwXoE7NlNguV/aPq1hFH72tqkHCw8eWXbr2xHspRyyv9DLpLXhq+Ieje+NwoqXrY0xyQjPXdOE6cGcHA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/core": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.2.4.tgz", + "integrity": "sha512-qdY3LpMOUyLM/gfjjMQZui+UTNS7kBRDWlvyIhVOql5dn2J3isk9qUTBtQ1CbDH8MTugHis1zu3h4rH+Qmmh4g==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/credential-provider-imds": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.1.3.tgz", + "integrity": "sha512-U1Yrv6hx/mRK6k8AncuI6jLUx9rn0VVSd9NPEX6pyYFBfkSkChOc/n4zUb8alHUVg83TbI4OdZVo1X0Zfj3ijA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.0.tgz", + "integrity": "sha512-vFvDxMrc6sO5Atec8PaISckMcAwsCrRhYxwUylg97bRT2KZoumOF7qk5+6EVUtuM1IG9AJV5aqXnHln9ZdXHpg==", + "dependencies": { + "@smithy/protocol-http": "^4.0.3", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/hash-node": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.3.tgz", + "integrity": "sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/hash-node/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/invalid-dependency": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz", + "integrity": "sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/middleware-content-length": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.3.tgz", + "integrity": "sha512-Dbz2bzexReYIQDWMr+gZhpwBetNXzbhnEMhYKA6urqmojO14CsXjnsoPYO8UL/xxcawn8ZsuVU61ElkLSltIUQ==", + "dependencies": { + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/middleware-endpoint": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.0.4.tgz", + "integrity": "sha512-whUJMEPwl3ANIbXjBXZVdJNgfV2ZU8ayln7xUM47rXL2txuenI7jQ/VFFwCzy5lCmXScjp6zYtptW5Evud8e9g==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/middleware-retry": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.7.tgz", + "integrity": "sha512-f5q7Y09G+2h5ivkSx5CHvlAT4qRR3jBFEsfXyQ9nFNiWQlr8c48blnu5cmbTQ+p1xmIO14UXzKoF8d7Tm0Gsjw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/middleware-serde": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", + "integrity": "sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/middleware-stack": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz", + "integrity": "sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/node-config-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.3.tgz", + "integrity": "sha512-rxdpAZczzholz6CYZxtqDu/aKTxATD5DAUDVj7HoEulq+pDSQVWzbg0btZDlxeFfa6bb2b5tUvgdX5+k8jUqcg==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/node-http-handler": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.1.tgz", + "integrity": "sha512-L71NLyPeP450r2J/mfu1jMc//Z1YnqJt2eSNw7uhiItaONnBLDA68J5jgxq8+MBDsYnFwNAIc7dBG1ImiWBiwg==", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/property-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.3.tgz", + "integrity": "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/protocol-http": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.0.3.tgz", + "integrity": "sha512-x5jmrCWwQlx+Zv4jAtc33ijJ+vqqYN+c/ZkrnpvEe/uDas7AT7A/4Rc2CdfxgWv4WFGmEqODIrrUToPN6DDkGw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/querystring-builder": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz", + "integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/querystring-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz", + "integrity": "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/service-error-classification": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz", + "integrity": "sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==", + "dependencies": { + "@smithy/types": "^3.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.3.tgz", + "integrity": "sha512-Z8Y3+08vgoDgl4HENqNnnzSISAaGrF2RoKupoC47u2wiMp+Z8P/8mDh1CL8+8ujfi2U5naNvopSBmP/BUj8b5w==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/signature-v4": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-3.1.2.tgz", + "integrity": "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/smithy-client": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.5.tgz", + "integrity": "sha512-x9bL9Mx2CT2P1OiUlHM+ZNpbVU6TgT32f9CmTRzqIHA7M4vYrROCWEoC3o4xHNJASoGd4Opos3cXYPgh+/m4Ww==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/url-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.3.tgz", + "integrity": "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-base64/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.7.tgz", + "integrity": "sha512-Q2txLyvQyGfmjsaDbVV7Sg8psefpFcrnlGapDzXGFRPFKRBeEg6OvFK8FljqjeHSaCZ6/UuzQExUPqBR/2qlDA==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.7.tgz", + "integrity": "sha512-F4Qcj1fG6MGi2BSWCslfsMSwllws/WzYONBGtLybyY+halAcXdWhcew+mej8M5SKd5hqPYp4f7b+ABQEaeytgg==", + "dependencies": { + "@smithy/config-resolver": "^3.0.4", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-endpoints": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.4.tgz", + "integrity": "sha512-ZAtNf+vXAsgzgRutDDiklU09ZzZiiV/nATyqde4Um4priTmasDH+eLpp3tspL0hS2dEootyFMhu1Y6Y+tzpWBQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.3.tgz", + "integrity": "sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-retry": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.3.tgz", + "integrity": "sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-stream": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.0.5.tgz", + "integrity": "sha512-xC3L5PKMAT/Bh8fmHNXP9sdQ4+4aKVUU3EEJ2CF/lLk7R+wtMJM+v/1B4en7jO++Wa5spGzFDBCl0QxgbUc5Ug==", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-stream/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/core": { "version": "3.496.0", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.496.0.tgz", @@ -1849,6 +2947,2437 @@ "tslib": "^2.3.1" } }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", + "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.1.tgz", + "integrity": "sha512-ExPSbgjwCoht6kB7B4MeZoBAxcQSIl29r/bPeazZJx50ej4JJCByimLOrZoIsurISNyJQQHf30b3JfqC3Hb88A==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.9.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/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/@azure/core-tracing": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", + "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.3.0.tgz", + "integrity": "sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/identity/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.8.0.tgz", + "integrity": "sha512-jkuYxgkw0aaRfk40OQhFqDIupqblIOIlYESWB6DKCVDxQet1pyv86Tfk9M+5uFM0+mCs6+MUHU+Hxh3joiUn4Q==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^2.0.1", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.8.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.2.tgz", + "integrity": "sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.18.0.tgz", + "integrity": "sha512-jvK5bDUWbpOaJt2Io/rjcaOVcUzkqkrCme/WntdV1SMUc67AiTcEdKuY6G/nMQ7N5Cfsk9SfpugflQwDku53yg==", + "dependencies": { + "@azure/msal-common": "14.13.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.13.0.tgz", + "integrity": "sha512-b4M/tqRzJ4jGU91BiwCsLTqChveUEyFK3qY2wGfZ0zBswIBZjAxopx5CYt5wzZFKuN15HqRDYXQbztttuIC3nA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.10.0.tgz", + "integrity": "sha512-JxsSE0464a8IA/+q5EHKmchwNyUFJHtCH00tSXsLaOddwLjG6yVvTH6lGgPcWMhO7YWUXj/XVgVgeE9kZtsPUQ==", + "dependencies": { + "@azure/msal-common": "14.13.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/msal-node/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_modules/@babel/cli": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.7.tgz", + "integrity": "sha512-8dfPprJgV4O14WTx+AQyEA+opgUKPrsIXX/MdL50J1n06EQJ6m1T+CdsJe0qEC0B/Xl85i+Un5KVAxd/PACX9A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "commander": "^6.2.0", + "convert-source-map": "^2.0.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/cli/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/cli/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@babel/cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@babel/cli/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/cli/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/cli/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", + "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@casl/ability": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", @@ -2506,6 +6035,14 @@ "yaml": "^2.2.2" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@hapi/bourne": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.1.0.tgz", @@ -2625,19 +6162,29 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -2648,9 +6195,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -2672,6 +6219,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@js-joda/core": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz", + "integrity": "sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA==" + }, "node_modules/@ldapjs/asn1": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz", @@ -2881,6 +6433,13 @@ "win32" ] }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, "node_modules/@node-saml/node-saml": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz", @@ -3913,54 +7472,41 @@ "dev": true }, "node_modules/@sindresorhus/slugify": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", - "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.0.tgz", + "integrity": "sha512-ujZRbmmizX26yS/HnB3P9QNlNa4+UvHh+rIse3RbOXLp8yl6n1TxB4t7NHggtVgS8QmmOtzXo48kCxZGACpkPw==", "dependencies": { - "@sindresorhus/transliterate": "^1.0.0", - "escape-string-regexp": "^5.0.0" + "@sindresorhus/transliterate": "^0.1.1", + "escape-string-regexp": "^4.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@sindresorhus/transliterate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", - "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz", + "integrity": "sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==", "dependencies": { - "escape-string-regexp": "^5.0.0" + "escape-string-regexp": "^2.0.0", + "lodash.deburr": "^4.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/@smithy/abort-controller": { @@ -4786,6 +8332,18 @@ "optional": true, "peer": true }, + "node_modules/@team-plain/typescript-sdk": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-4.6.1.tgz", + "integrity": "sha512-Uy9QJXu9U7bJb6WXL9sArGk7FXPpzdqBd6q8tAF1vexTm8fbTJRqcikTKxGtZmNADt+C2SapH3cApM4oHpO4lQ==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "graphql": "^16.6.0", + "zod": "3.22.4" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -5133,6 +8691,20 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, + "node_modules/@types/readable-stream": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", + "integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -5642,6 +9214,212 @@ "node": ">=10.0.0" } }, + "node_modules/@yao-pkg/pkg": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.12.0.tgz", + "integrity": "sha512-KZVpiDKRi2gtrVtKwhz/ZUKBOicVNggxaYQzPBjULuOLJ/UypTmAz5a2g+utLMn+WogbLE3vLfmC+TWp8v3+aQ==", + "dev": true, + "dependencies": { + "@babel/generator": "7.23.0", + "@babel/parser": "7.23.0", + "@babel/types": "7.23.0", + "@yao-pkg/pkg-fetch": "3.5.9", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimatch": "9.0.4", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + } + }, + "node_modules/@yao-pkg/pkg-fetch": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.9.tgz", + "integrity": "sha512-usMwwqFCd2B7k+V87u6kiTesyDSlw+3LpiuYBWe+UgryvSOk/NXjx3XVCub8hQoi0bCREbdQ6NDBqminyHJJrg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/@yao-pkg/pkg-fetch/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@yao-pkg/pkg-fetch/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@yao-pkg/pkg-fetch/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@yao-pkg/pkg/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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -6134,6 +9912,15 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -6286,6 +10073,67 @@ "axios": "0.x || 1.x" } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-import-meta": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.2.1.tgz", + "integrity": "sha512-AxNh27Pcg8Kt112RGa3Vod2QS2YXKKJ6+nSvRtv7qQTJAdx0MZa4UHZ4lnxHUWA2MNbLuZQv5FVab4P1CoLOWw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.4.4", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.0" + } + }, "node_modules/backoff": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", @@ -6470,6 +10318,38 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/btoa-lite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", @@ -6588,6 +10468,26 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001639", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", + "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/cassandra-driver": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz", @@ -6720,6 +10620,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -6790,6 +10759,17 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, + "node_modules/connect-redis": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.1.tgz", + "integrity": "sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "express-session": ">=1" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -6814,6 +10794,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -6830,6 +10816,19 @@ "node": ">=6.6.0" } }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -6904,6 +10903,21 @@ "ms": "^2.1.1" } }, + "node_modules/decompress-response": { + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", @@ -6916,6 +10930,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "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" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6943,6 +10966,14 @@ "node": ">= 0.4" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -7106,6 +11137,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/electron-to-chromium": { + "version": "1.4.816", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz", + "integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==", + "dev": true + }, "node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", @@ -7279,9 +11316,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -7295,7 +11332,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -7855,6 +11891,15 @@ "node": ">=12.0.0" } }, + "node_modules/expand-template": { + "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" + } + }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -7896,6 +11941,55 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "peer": true, + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "peer": true + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "peer": true + }, "node_modules/express/node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -8324,6 +12418,73 @@ "node": ">= 0.6" } }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -8346,6 +12507,12 @@ "node": ">=8" } }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8546,6 +12713,15 @@ "is-property": "^1.0.2" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -8630,6 +12806,12 @@ "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" }, + "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==", + "dev": true + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -8820,6 +13002,14 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/gtoken": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", @@ -8871,6 +13061,15 @@ "uglify-js": "^3.1.4" } }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -9024,6 +13223,50 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/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/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -9158,6 +13401,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/internal-slot": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", @@ -9180,6 +13429,22 @@ "node": ">= 0.10" } }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ioredis": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", @@ -9343,6 +13608,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9565,6 +13844,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -9603,6 +13893,14 @@ "node": ">= 0.6.0" } }, + "node_modules/jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -9611,6 +13909,17 @@ "node": ">=10" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -9627,6 +13936,18 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -9720,6 +14041,18 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -10062,6 +14395,17 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -10326,6 +14670,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10387,6 +14743,12 @@ "node": ">=10" } }, + "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==", + "dev": true + }, "node_modules/mlly": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", @@ -10449,6 +14811,44 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/multistream/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", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/mylas": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", @@ -10520,9 +14920,9 @@ } }, "node_modules/nanoid": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz", - "integrity": "sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -10530,12 +14930,23 @@ } ], "bin": { - "nanoid": "bin/nanoid.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^18 || >=20" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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==", + "dev": true + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10555,6 +14966,18 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/node-abi": { + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", + "integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -10595,6 +15018,12 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/nodemailer": { "version": "6.9.9", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.9.tgz", @@ -10728,6 +15157,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -10851,6 +15288,14 @@ "@octokit/core": ">=5" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -10870,6 +15315,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10892,11 +15346,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" }, + "node_modules/openid-client": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", + "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "dependencies": { + "jose": "^4.15.5", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -10945,6 +15429,15 @@ "node": ">=14.6" } }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -11326,9 +15819,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -11594,24 +16087,6 @@ } } }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -11665,6 +16140,32 @@ "node": ">=15.0.0" } }, + "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", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/precond": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", @@ -11800,11 +16301,26 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, "node_modules/process-warning": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prompt-sync": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", @@ -11948,6 +16464,15 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11989,6 +16514,30 @@ "node": ">=0.10.0" } }, + "node_modules/rc": { + "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", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "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" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -12077,6 +16626,39 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -12094,6 +16676,53 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -12571,6 +17200,51 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-concat": { + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -12646,6 +17320,11 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -12693,6 +17372,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/stream-meter/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/stream-meter/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-meter/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/stream-meter/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", @@ -13007,6 +17740,89 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "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", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/tar-stream": { + "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", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/bl": { + "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", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/tar-stream/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", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tarn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", @@ -13015,6 +17831,36 @@ "node": ">=8.0.0" } }, + "node_modules/tedious": { + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-18.2.3.tgz", + "integrity": "sha512-AMdO1sodcoMU3vqDiU2d+Bdck6LcMAj4s4/fkxWXAgWGVnbZOQKaQrn6f+cRAZpdJhn5b8vX7cOfmB7oKNMUqQ==", + "dependencies": { + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.1", + "@types/node": ">=18", + "bl": "^6.0.11", + "iconv-lite": "^0.6.3", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tedious/node_modules/bl": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.13.tgz", + "integrity": "sha512-tMncAcpsyjZgAVbVFupVIaB2xud13xxT59fdHkuszY2jdZkqIWfpQdmII1fOe3kOGAz0mNLTIHEm+KxpYsQKKg==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13082,6 +17928,15 @@ "node": ">=14.0.0" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13170,9 +18025,9 @@ "dev": true }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -13876,6 +18731,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/tunnel-agent": { + "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" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -14027,6 +18894,18 @@ "node": ">=0.8.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "peer": true, + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uid2": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", @@ -14058,6 +18937,46 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/universal-github-app-jwt": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz", @@ -14072,6 +18991,15 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -14080,6 +19008,36 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/update-dotenv": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-dotenv/-/update-dotenv-1.1.1.tgz", @@ -15245,6 +20203,15 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -15258,6 +20225,74 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index f2a8582d6..b2f3cba0c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,11 +3,39 @@ "version": "1.0.0", "description": "", "main": "./dist/main.mjs", + "bin": "dist/main.js", + "pkg": { + "scripts": [ + "dist/**/*.js", + "../frontend/node_modules/next/**/*.js", + "../frontend/.next/*/**/*.js", + "../frontend/node_modules/next/dist/server/**/*.js", + "../frontend/node_modules/@fortawesome/fontawesome-svg-core/**/*.js" + ], + "assets": [ + "dist/**", + "!dist/**/*.js", + "node_modules/**", + "../frontend/node_modules/**", + "../frontend/.next/**", + "!../frontend/node_modules/next/dist/server/**/*.js", + "../frontend/node_modules/@fortawesome/fontawesome-svg-core/**/*", + "../frontend/public/**" + ], + "outputPath": "binary" + }, "scripts": { + "binary:build": "npm run binary:clean && npm run build:frontend && npm run build && npm run binary:babel-frontend && npm run binary:babel-backend && npm run binary:rename-imports", + "binary:package": "pkg --no-bytecode --public-packages \"*\" --public --target host .", + "binary:babel-backend": " babel ./dist -d ./dist", + "binary:babel-frontend": "babel --copy-files ../frontend/.next/server -d ../frontend/.next/server", + "binary:clean": "rm -rf ./dist && rm -rf ./binary", + "binary:rename-imports": "ts-node ./scripts/rename-mjs.ts", "test": "echo \"Error: no test specified\" && exit 1", "dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine", "dev:docker": "nodemon", "build": "tsup", + "build:frontend": "npm run build --prefix ../frontend", "start": "node dist/main.mjs", "type:check": "tsc --noEmit", "lint:fix": "eslint --fix --ext js,ts ./src", @@ -31,6 +59,11 @@ "author": "", "license": "ISC", "devDependencies": { + "@babel/cli": "^7.18.10", + "@babel/core": "^7.18.10", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/preset-env": "^7.18.10", + "@babel/preset-react": "^7.24.7", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "^9.0.5", @@ -48,6 +81,8 @@ "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", + "@yao-pkg/pkg": "^5.12.0", + "babel-plugin-transform-import-meta": "^2.2.1", "eslint": "^8.56.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -60,7 +95,7 @@ "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", "rimraf": "^5.0.5", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", "tsconfig-paths": "^4.2.0", "tsup": "^8.0.1", @@ -72,6 +107,7 @@ "dependencies": { "@aws-sdk/client-iam": "^3.525.0", "@aws-sdk/client-secrets-manager": "^3.504.0", + "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", @@ -89,7 +125,8 @@ "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.10.0", "@serdnam/pino-cloudwatch-transport": "^1.0.4", - "@sindresorhus/slugify": "^2.2.1", + "@team-plain/typescript-sdk": "^4.6.1", + "@sindresorhus/slugify": "1.1.0", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -99,6 +136,7 @@ "bcrypt": "^5.1.1", "bullmq": "^5.4.2", "cassandra-driver": "^4.7.2", + "connect-redis": "^7.1.1", "cron": "^3.1.7", "dotenv": "^16.4.1", "fastify": "^4.26.0", @@ -116,8 +154,9 @@ "lodash.isequal": "^4.5.0", "ms": "^2.1.3", "mysql2": "^3.9.8", - "nanoid": "^5.0.4", + "nanoid": "^3.3.4", "nodemailer": "^6.9.9", + "openid-client": "^5.6.5", "ora": "^7.0.1", "oracledb": "^6.4.0", "passport-github": "^1.1.0", @@ -131,6 +170,7 @@ "posthog-node": "^3.6.2", "probot": "^13.0.0", "smee-client": "^2.0.0", + "tedious": "^18.2.1", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "uuid": "^9.0.1", diff --git a/backend/scripts/create-migration.ts b/backend/scripts/create-migration.ts index 59040a37a..34f4aca41 100644 --- a/backend/scripts/create-migration.ts +++ b/backend/scripts/create-migration.ts @@ -2,13 +2,14 @@ import { execSync } from "child_process"; import path from "path"; import promptSync from "prompt-sync"; +import slugify from "@sindresorhus/slugify" const prompt = promptSync({ sigint: true }); const migrationName = prompt("Enter name for migration: "); // Remove spaces from migration name and replace with hyphens -const formattedMigrationName = migrationName.replace(/\s+/g, "-"); +const formattedMigrationName = slugify(migrationName); execSync( `npx knex migrate:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${formattedMigrationName}`, diff --git a/backend/scripts/rename-mjs.ts b/backend/scripts/rename-mjs.ts new file mode 100644 index 000000000..793cb9891 --- /dev/null +++ b/backend/scripts/rename-mjs.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import fs from "node:fs"; +import path from "node:path"; + +function replaceMjsOccurrences(directory: string) { + fs.readdir(directory, (err, files) => { + if (err) throw err; + files.forEach((file) => { + const filePath = path.join(directory, file); + if (fs.statSync(filePath).isDirectory()) { + replaceMjsOccurrences(filePath); + } else { + fs.readFile(filePath, "utf8", (err, data) => { + if (err) throw err; + const result = data.replace(/\.mjs/g, ".js"); + fs.writeFile(filePath, result, "utf8", (err) => { + if (err) throw err; + // eslint-disable-next-line no-console + console.log(`Updated: ${filePath}`); + }); + }); + } + }); + }); +} + +replaceMjsOccurrences("dist"); diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index e5cf3f79f..166c5a3f9 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -13,6 +13,7 @@ import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; @@ -64,6 +65,7 @@ import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserServiceFactory } from "@app/services/user/user-service"; +import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; declare module "fastify" { @@ -102,6 +104,7 @@ declare module "fastify" { permission: TPermissionServiceFactory; org: TOrgServiceFactory; orgRole: TOrgRoleServiceFactory; + oidc: TOidcConfigServiceFactory; superAdmin: TSuperAdminServiceFactory; user: TUserServiceFactory; group: TGroupServiceFactory; @@ -155,6 +158,7 @@ declare module "fastify" { identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; + userEngagement: TUserEngagementServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 79342ea27..82ca89742 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -1,4 +1,4 @@ -import { Knex } from "knex"; +import { Knex as KnexOriginal } from "knex"; import { TableName, @@ -134,6 +134,9 @@ import { TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate, + TOidcConfigs, + TOidcConfigsInsert, + TOidcConfigsUpdate, TOrganizations, TOrganizationsInsert, TOrganizationsUpdate, @@ -277,317 +280,371 @@ import { TWebhooksUpdate } from "@app/db/schemas"; +declare module "knex" { + namespace Knex { + interface QueryInterface { + primaryNode(): KnexOriginal; + replicaNode(): KnexOriginal; + } + } +} + declare module "knex/types/tables" { interface Tables { - [TableName.Users]: Knex.CompositeTableType; - [TableName.Groups]: Knex.CompositeTableType; - [TableName.CertificateAuthority]: Knex.CompositeTableType< + [TableName.Users]: KnexOriginal.CompositeTableType; + [TableName.Groups]: KnexOriginal.CompositeTableType; + [TableName.CertificateAuthority]: KnexOriginal.CompositeTableType< TCertificateAuthorities, TCertificateAuthoritiesInsert, TCertificateAuthoritiesUpdate >; - [TableName.CertificateAuthorityCert]: Knex.CompositeTableType< + [TableName.CertificateAuthorityCert]: KnexOriginal.CompositeTableType< TCertificateAuthorityCerts, TCertificateAuthorityCertsInsert, TCertificateAuthorityCertsUpdate >; - [TableName.CertificateAuthoritySecret]: Knex.CompositeTableType< + [TableName.CertificateAuthoritySecret]: KnexOriginal.CompositeTableType< TCertificateAuthoritySecret, TCertificateAuthoritySecretInsert, TCertificateAuthoritySecretUpdate >; - [TableName.CertificateAuthorityCrl]: Knex.CompositeTableType< + [TableName.CertificateAuthorityCrl]: KnexOriginal.CompositeTableType< TCertificateAuthorityCrl, TCertificateAuthorityCrlInsert, TCertificateAuthorityCrlUpdate >; - [TableName.Certificate]: Knex.CompositeTableType; - [TableName.CertificateBody]: Knex.CompositeTableType< + [TableName.Certificate]: KnexOriginal.CompositeTableType; + [TableName.CertificateBody]: KnexOriginal.CompositeTableType< TCertificateBodies, TCertificateBodiesInsert, TCertificateBodiesUpdate >; - [TableName.CertificateSecret]: Knex.CompositeTableType< + [TableName.CertificateSecret]: KnexOriginal.CompositeTableType< TCertificateSecrets, TCertificateSecretsInsert, TCertificateSecretsUpdate >; - [TableName.UserGroupMembership]: Knex.CompositeTableType< + [TableName.UserGroupMembership]: KnexOriginal.CompositeTableType< TUserGroupMembership, TUserGroupMembershipInsert, TUserGroupMembershipUpdate >; - [TableName.GroupProjectMembership]: Knex.CompositeTableType< + [TableName.GroupProjectMembership]: KnexOriginal.CompositeTableType< TGroupProjectMemberships, TGroupProjectMembershipsInsert, TGroupProjectMembershipsUpdate >; - [TableName.GroupProjectMembershipRole]: Knex.CompositeTableType< + [TableName.GroupProjectMembershipRole]: KnexOriginal.CompositeTableType< TGroupProjectMembershipRoles, TGroupProjectMembershipRolesInsert, TGroupProjectMembershipRolesUpdate >; - [TableName.UserAliases]: Knex.CompositeTableType; - [TableName.UserEncryptionKey]: Knex.CompositeTableType< + [TableName.UserAliases]: KnexOriginal.CompositeTableType; + [TableName.UserEncryptionKey]: KnexOriginal.CompositeTableType< TUserEncryptionKeys, TUserEncryptionKeysInsert, TUserEncryptionKeysUpdate >; - [TableName.AuthTokens]: Knex.CompositeTableType; - [TableName.AuthTokenSession]: Knex.CompositeTableType< + [TableName.AuthTokens]: KnexOriginal.CompositeTableType; + [TableName.AuthTokenSession]: KnexOriginal.CompositeTableType< TAuthTokenSessions, TAuthTokenSessionsInsert, TAuthTokenSessionsUpdate >; - [TableName.BackupPrivateKey]: Knex.CompositeTableType< + [TableName.BackupPrivateKey]: KnexOriginal.CompositeTableType< TBackupPrivateKey, TBackupPrivateKeyInsert, TBackupPrivateKeyUpdate >; - [TableName.Organization]: Knex.CompositeTableType; - [TableName.OrgMembership]: Knex.CompositeTableType; - [TableName.OrgRoles]: Knex.CompositeTableType; - [TableName.IncidentContact]: Knex.CompositeTableType< + [TableName.Organization]: KnexOriginal.CompositeTableType< + TOrganizations, + TOrganizationsInsert, + TOrganizationsUpdate + >; + [TableName.OrgMembership]: KnexOriginal.CompositeTableType< + TOrgMemberships, + TOrgMembershipsInsert, + TOrgMembershipsUpdate + >; + [TableName.OrgRoles]: KnexOriginal.CompositeTableType; + [TableName.IncidentContact]: KnexOriginal.CompositeTableType< TIncidentContacts, TIncidentContactsInsert, TIncidentContactsUpdate >; - [TableName.UserAction]: Knex.CompositeTableType; - [TableName.SuperAdmin]: Knex.CompositeTableType; - [TableName.ApiKey]: Knex.CompositeTableType; - [TableName.Project]: Knex.CompositeTableType; - [TableName.ProjectMembership]: Knex.CompositeTableType< + [TableName.UserAction]: KnexOriginal.CompositeTableType; + [TableName.SuperAdmin]: KnexOriginal.CompositeTableType; + [TableName.ApiKey]: KnexOriginal.CompositeTableType; + [TableName.Project]: KnexOriginal.CompositeTableType; + [TableName.ProjectMembership]: KnexOriginal.CompositeTableType< TProjectMemberships, TProjectMembershipsInsert, TProjectMembershipsUpdate >; - [TableName.Environment]: Knex.CompositeTableType< + [TableName.Environment]: KnexOriginal.CompositeTableType< TProjectEnvironments, TProjectEnvironmentsInsert, TProjectEnvironmentsUpdate >; - [TableName.ProjectBot]: Knex.CompositeTableType; - [TableName.ProjectUserMembershipRole]: Knex.CompositeTableType< + [TableName.ProjectBot]: KnexOriginal.CompositeTableType; + [TableName.ProjectUserMembershipRole]: KnexOriginal.CompositeTableType< TProjectUserMembershipRoles, TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesUpdate >; - [TableName.ProjectRoles]: Knex.CompositeTableType; - [TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType< + [TableName.ProjectRoles]: KnexOriginal.CompositeTableType; + [TableName.ProjectUserAdditionalPrivilege]: KnexOriginal.CompositeTableType< TProjectUserAdditionalPrivilege, TProjectUserAdditionalPrivilegeInsert, TProjectUserAdditionalPrivilegeUpdate >; - [TableName.ProjectKeys]: Knex.CompositeTableType; - [TableName.Secret]: Knex.CompositeTableType; - [TableName.SecretReference]: Knex.CompositeTableType< + [TableName.ProjectKeys]: KnexOriginal.CompositeTableType; + [TableName.Secret]: KnexOriginal.CompositeTableType; + [TableName.SecretReference]: KnexOriginal.CompositeTableType< TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate >; - [TableName.SecretBlindIndex]: Knex.CompositeTableType< + [TableName.SecretBlindIndex]: KnexOriginal.CompositeTableType< TSecretBlindIndexes, TSecretBlindIndexesInsert, TSecretBlindIndexesUpdate >; - [TableName.SecretVersion]: Knex.CompositeTableType; - [TableName.SecretFolder]: Knex.CompositeTableType; - [TableName.SecretFolderVersion]: Knex.CompositeTableType< + [TableName.SecretVersion]: KnexOriginal.CompositeTableType< + TSecretVersions, + TSecretVersionsInsert, + TSecretVersionsUpdate + >; + [TableName.SecretFolder]: KnexOriginal.CompositeTableType< + TSecretFolders, + TSecretFoldersInsert, + TSecretFoldersUpdate + >; + [TableName.SecretFolderVersion]: KnexOriginal.CompositeTableType< TSecretFolderVersions, TSecretFolderVersionsInsert, TSecretFolderVersionsUpdate >; - [TableName.SecretSharing]: Knex.CompositeTableType; - [TableName.RateLimit]: Knex.CompositeTableType; - [TableName.SecretTag]: Knex.CompositeTableType; - [TableName.SecretImport]: Knex.CompositeTableType; - [TableName.Integration]: Knex.CompositeTableType; - [TableName.Webhook]: Knex.CompositeTableType; - [TableName.ServiceToken]: Knex.CompositeTableType; - [TableName.IntegrationAuth]: Knex.CompositeTableType< + [TableName.SecretSharing]: KnexOriginal.CompositeTableType< + TSecretSharing, + TSecretSharingInsert, + TSecretSharingUpdate + >; + [TableName.RateLimit]: KnexOriginal.CompositeTableType; + [TableName.SecretTag]: KnexOriginal.CompositeTableType; + [TableName.SecretImport]: KnexOriginal.CompositeTableType< + TSecretImports, + TSecretImportsInsert, + TSecretImportsUpdate + >; + [TableName.Integration]: KnexOriginal.CompositeTableType; + [TableName.Webhook]: KnexOriginal.CompositeTableType; + [TableName.ServiceToken]: KnexOriginal.CompositeTableType< + TServiceTokens, + TServiceTokensInsert, + TServiceTokensUpdate + >; + [TableName.IntegrationAuth]: KnexOriginal.CompositeTableType< TIntegrationAuths, TIntegrationAuthsInsert, TIntegrationAuthsUpdate >; - [TableName.Identity]: Knex.CompositeTableType; - [TableName.IdentityUniversalAuth]: Knex.CompositeTableType< + [TableName.Identity]: KnexOriginal.CompositeTableType; + [TableName.IdentityUniversalAuth]: KnexOriginal.CompositeTableType< TIdentityUniversalAuths, TIdentityUniversalAuthsInsert, TIdentityUniversalAuthsUpdate >; - [TableName.IdentityKubernetesAuth]: Knex.CompositeTableType< + [TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType< TIdentityKubernetesAuths, TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsUpdate >; - [TableName.IdentityGcpAuth]: Knex.CompositeTableType< + [TableName.IdentityGcpAuth]: KnexOriginal.CompositeTableType< TIdentityGcpAuths, TIdentityGcpAuthsInsert, TIdentityGcpAuthsUpdate >; - [TableName.IdentityAwsAuth]: Knex.CompositeTableType< + [TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType< TIdentityAwsAuths, TIdentityAwsAuthsInsert, TIdentityAwsAuthsUpdate >; - [TableName.IdentityAzureAuth]: Knex.CompositeTableType< + [TableName.IdentityAzureAuth]: KnexOriginal.CompositeTableType< TIdentityAzureAuths, TIdentityAzureAuthsInsert, TIdentityAzureAuthsUpdate >; - [TableName.IdentityUaClientSecret]: Knex.CompositeTableType< + [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, TIdentityUaClientSecretsUpdate >; - [TableName.IdentityAccessToken]: Knex.CompositeTableType< + [TableName.IdentityAccessToken]: KnexOriginal.CompositeTableType< TIdentityAccessTokens, TIdentityAccessTokensInsert, TIdentityAccessTokensUpdate >; - [TableName.IdentityOrgMembership]: Knex.CompositeTableType< + [TableName.IdentityOrgMembership]: KnexOriginal.CompositeTableType< TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate >; - [TableName.IdentityProjectMembership]: Knex.CompositeTableType< + [TableName.IdentityProjectMembership]: KnexOriginal.CompositeTableType< TIdentityProjectMemberships, TIdentityProjectMembershipsInsert, TIdentityProjectMembershipsUpdate >; - [TableName.IdentityProjectMembershipRole]: Knex.CompositeTableType< + [TableName.IdentityProjectMembershipRole]: KnexOriginal.CompositeTableType< TIdentityProjectMembershipRole, TIdentityProjectMembershipRoleInsert, TIdentityProjectMembershipRoleUpdate >; - [TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType< + [TableName.IdentityProjectAdditionalPrivilege]: KnexOriginal.CompositeTableType< TIdentityProjectAdditionalPrivilege, TIdentityProjectAdditionalPrivilegeInsert, TIdentityProjectAdditionalPrivilegeUpdate >; - [TableName.AccessApprovalPolicy]: Knex.CompositeTableType< + [TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType< TAccessApprovalPolicies, TAccessApprovalPoliciesInsert, TAccessApprovalPoliciesUpdate >; - [TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType< + [TableName.AccessApprovalPolicyApprover]: KnexOriginal.CompositeTableType< TAccessApprovalPoliciesApprovers, TAccessApprovalPoliciesApproversInsert, TAccessApprovalPoliciesApproversUpdate >; - [TableName.AccessApprovalRequest]: Knex.CompositeTableType< + [TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType< TAccessApprovalRequests, TAccessApprovalRequestsInsert, TAccessApprovalRequestsUpdate >; - [TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType< + [TableName.AccessApprovalRequestReviewer]: KnexOriginal.CompositeTableType< TAccessApprovalRequestsReviewers, TAccessApprovalRequestsReviewersInsert, TAccessApprovalRequestsReviewersUpdate >; - [TableName.ScimToken]: Knex.CompositeTableType; - [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< + [TableName.ScimToken]: KnexOriginal.CompositeTableType; + [TableName.SecretApprovalPolicy]: KnexOriginal.CompositeTableType< TSecretApprovalPolicies, TSecretApprovalPoliciesInsert, TSecretApprovalPoliciesUpdate >; - [TableName.SecretApprovalPolicyApprover]: Knex.CompositeTableType< + [TableName.SecretApprovalPolicyApprover]: KnexOriginal.CompositeTableType< TSecretApprovalPoliciesApprovers, TSecretApprovalPoliciesApproversInsert, TSecretApprovalPoliciesApproversUpdate >; - [TableName.SecretApprovalRequest]: Knex.CompositeTableType< + [TableName.SecretApprovalRequest]: KnexOriginal.CompositeTableType< TSecretApprovalRequests, TSecretApprovalRequestsInsert, TSecretApprovalRequestsUpdate >; - [TableName.SecretApprovalRequestReviewer]: Knex.CompositeTableType< + [TableName.SecretApprovalRequestReviewer]: KnexOriginal.CompositeTableType< TSecretApprovalRequestsReviewers, TSecretApprovalRequestsReviewersInsert, TSecretApprovalRequestsReviewersUpdate >; - [TableName.SecretApprovalRequestSecret]: Knex.CompositeTableType< + [TableName.SecretApprovalRequestSecret]: KnexOriginal.CompositeTableType< TSecretApprovalRequestsSecrets, TSecretApprovalRequestsSecretsInsert, TSecretApprovalRequestsSecretsUpdate >; - [TableName.SecretApprovalRequestSecretTag]: Knex.CompositeTableType< + [TableName.SecretApprovalRequestSecretTag]: KnexOriginal.CompositeTableType< TSecretApprovalRequestSecretTags, TSecretApprovalRequestSecretTagsInsert, TSecretApprovalRequestSecretTagsUpdate >; - [TableName.SecretRotation]: Knex.CompositeTableType< + [TableName.SecretRotation]: KnexOriginal.CompositeTableType< TSecretRotations, TSecretRotationsInsert, TSecretRotationsUpdate >; - [TableName.SecretRotationOutput]: Knex.CompositeTableType< + [TableName.SecretRotationOutput]: KnexOriginal.CompositeTableType< TSecretRotationOutputs, TSecretRotationOutputsInsert, TSecretRotationOutputsUpdate >; - [TableName.Snapshot]: Knex.CompositeTableType; - [TableName.SnapshotSecret]: Knex.CompositeTableType< + [TableName.Snapshot]: KnexOriginal.CompositeTableType< + TSecretSnapshots, + TSecretSnapshotsInsert, + TSecretSnapshotsUpdate + >; + [TableName.SnapshotSecret]: KnexOriginal.CompositeTableType< TSecretSnapshotSecrets, TSecretSnapshotSecretsInsert, TSecretSnapshotSecretsUpdate >; - [TableName.SnapshotFolder]: Knex.CompositeTableType< + [TableName.SnapshotFolder]: KnexOriginal.CompositeTableType< TSecretSnapshotFolders, TSecretSnapshotFoldersInsert, TSecretSnapshotFoldersUpdate >; - [TableName.DynamicSecret]: Knex.CompositeTableType; - [TableName.DynamicSecretLease]: Knex.CompositeTableType< + [TableName.DynamicSecret]: KnexOriginal.CompositeTableType< + TDynamicSecrets, + TDynamicSecretsInsert, + TDynamicSecretsUpdate + >; + [TableName.DynamicSecretLease]: KnexOriginal.CompositeTableType< TDynamicSecretLeases, TDynamicSecretLeasesInsert, TDynamicSecretLeasesUpdate >; - [TableName.SamlConfig]: Knex.CompositeTableType; - [TableName.LdapConfig]: Knex.CompositeTableType; - [TableName.LdapGroupMap]: Knex.CompositeTableType; - [TableName.OrgBot]: Knex.CompositeTableType; - [TableName.AuditLog]: Knex.CompositeTableType; - [TableName.AuditLogStream]: Knex.CompositeTableType< + [TableName.SamlConfig]: KnexOriginal.CompositeTableType; + [TableName.OidcConfig]: KnexOriginal.CompositeTableType; + [TableName.LdapConfig]: KnexOriginal.CompositeTableType; + [TableName.LdapGroupMap]: KnexOriginal.CompositeTableType< + TLdapGroupMaps, + TLdapGroupMapsInsert, + TLdapGroupMapsUpdate + >; + [TableName.OrgBot]: KnexOriginal.CompositeTableType; + [TableName.AuditLog]: KnexOriginal.CompositeTableType; + [TableName.AuditLogStream]: KnexOriginal.CompositeTableType< TAuditLogStreams, TAuditLogStreamsInsert, TAuditLogStreamsUpdate >; - [TableName.GitAppInstallSession]: Knex.CompositeTableType< + [TableName.GitAppInstallSession]: KnexOriginal.CompositeTableType< TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate >; - [TableName.GitAppOrg]: Knex.CompositeTableType; - [TableName.SecretScanningGitRisk]: Knex.CompositeTableType< + [TableName.GitAppOrg]: KnexOriginal.CompositeTableType; + [TableName.SecretScanningGitRisk]: KnexOriginal.CompositeTableType< TSecretScanningGitRisks, TSecretScanningGitRisksInsert, TSecretScanningGitRisksUpdate >; - [TableName.TrustedIps]: Knex.CompositeTableType; + [TableName.TrustedIps]: KnexOriginal.CompositeTableType; // Junction tables - [TableName.JnSecretTag]: Knex.CompositeTableType< + [TableName.JnSecretTag]: KnexOriginal.CompositeTableType< TSecretTagJunction, TSecretTagJunctionInsert, TSecretTagJunctionUpdate >; - [TableName.SecretVersionTag]: Knex.CompositeTableType< + [TableName.SecretVersionTag]: KnexOriginal.CompositeTableType< TSecretVersionTagJunction, TSecretVersionTagJunctionInsert, TSecretVersionTagJunctionUpdate >; // KMS service - [TableName.KmsServerRootConfig]: Knex.CompositeTableType< + [TableName.KmsServerRootConfig]: KnexOriginal.CompositeTableType< TKmsRootConfig, TKmsRootConfigInsert, TKmsRootConfigUpdate >; - [TableName.KmsKey]: Knex.CompositeTableType; - [TableName.KmsKeyVersion]: Knex.CompositeTableType; + [TableName.KmsKey]: KnexOriginal.CompositeTableType; + [TableName.KmsKeyVersion]: KnexOriginal.CompositeTableType< + TKmsKeyVersions, + TKmsKeyVersionsInsert, + TKmsKeyVersionsUpdate + >; } } diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index bd4ce99c1..f6162ad9c 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -1,8 +1,38 @@ -import knex from "knex"; +import knex, { Knex } from "knex"; export type TDbClient = ReturnType; -export const initDbConnection = ({ dbConnectionUri, dbRootCert }: { dbConnectionUri: string; dbRootCert?: string }) => { - const db = knex({ +export const initDbConnection = ({ + dbConnectionUri, + dbRootCert, + readReplicas = [] +}: { + dbConnectionUri: string; + dbRootCert?: string; + readReplicas?: { + dbConnectionUri: string; + dbRootCert?: string; + }[]; +}) => { + // akhilmhdh: the default Knex is knex.Knex. but when assigned with knex({}) the value is knex.Knex + // this was causing issue with files like `snapshot-dal` `findRecursivelySnapshots` this i am explicitly putting the any and unknown[] + // eslint-disable-next-line + let db: Knex; + // eslint-disable-next-line + let readReplicaDbs: Knex[]; + // @ts-expect-error the querybuilder type is expected but our intension is to return a knex instance + knex.QueryBuilder.extend("primaryNode", () => { + return db; + }); + + // @ts-expect-error the querybuilder type is expected but our intension is to return a knex instance + knex.QueryBuilder.extend("replicaNode", () => { + if (!readReplicaDbs.length) return db; + + const selectedReplica = readReplicaDbs[Math.floor(Math.random() * readReplicaDbs.length)]; + return selectedReplica; + }); + + db = knex({ client: "pg", connection: { connectionString: dbConnectionUri, @@ -22,5 +52,21 @@ export const initDbConnection = ({ dbConnectionUri, dbRootCert }: { dbConnection } }); + readReplicaDbs = readReplicas.map((el) => { + const replicaDbCertificate = el.dbRootCert || dbRootCert; + return knex({ + client: "pg", + connection: { + connectionString: el.dbConnectionUri, + ssl: replicaDbCertificate + ? { + rejectUnauthorized: true, + ca: Buffer.from(replicaDbCertificate, "base64").toString("ascii") + } + : false + } + }); + }); + return db; }; diff --git a/backend/src/db/migrations/20240624161942_add-oidc-auth.ts b/backend/src/db/migrations/20240624161942_add-oidc-auth.ts new file mode 100644 index 000000000..3f4b0636d --- /dev/null +++ b/backend/src/db/migrations/20240624161942_add-oidc-auth.ts @@ -0,0 +1,49 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.OidcConfig))) { + await knex.schema.createTable(TableName.OidcConfig, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.string("discoveryURL"); + tb.string("issuer"); + tb.string("authorizationEndpoint"); + tb.string("jwksUri"); + tb.string("tokenEndpoint"); + tb.string("userinfoEndpoint"); + tb.text("encryptedClientId").notNullable(); + tb.string("configurationType").notNullable(); + tb.string("clientIdIV").notNullable(); + tb.string("clientIdTag").notNullable(); + tb.text("encryptedClientSecret").notNullable(); + tb.string("clientSecretIV").notNullable(); + tb.string("clientSecretTag").notNullable(); + tb.string("allowedEmailDomains").nullable(); + tb.boolean("isActive").notNullable(); + tb.timestamps(true, true, true); + tb.uuid("orgId").notNullable().unique(); + tb.foreign("orgId").references("id").inTable(TableName.Organization); + }); + } + + if (await knex.schema.hasTable(TableName.SuperAdmin)) { + if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails"))) { + await knex.schema.alterTable(TableName.SuperAdmin, (tb) => { + tb.boolean("trustOidcEmails").defaultTo(false); + }); + } + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.OidcConfig); + + if (await knex.schema.hasTable(TableName.SuperAdmin)) { + if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("trustOidcEmails"); + }); + } + } +} diff --git a/backend/src/db/migrations/20240624172027_default-saml-ldap-org.ts b/backend/src/db/migrations/20240624172027_default-saml-ldap-org.ts new file mode 100644 index 000000000..fec132df4 --- /dev/null +++ b/backend/src/db/migrations/20240624172027_default-saml-ldap-org.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +const DEFAULT_AUTH_ORG_ID_FIELD = "defaultAuthOrgId"; + +export async function up(knex: Knex): Promise { + const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD); + + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (!hasDefaultOrgColumn) { + t.uuid(DEFAULT_AUTH_ORG_ID_FIELD).nullable(); + t.foreign(DEFAULT_AUTH_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL"); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD); + + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (hasDefaultOrgColumn) { + t.dropForeign([DEFAULT_AUTH_ORG_ID_FIELD]); + t.dropColumn(DEFAULT_AUTH_ORG_ID_FIELD); + } + }); +} diff --git a/backend/src/db/migrations/20240624221840_certificate-alt-names.ts b/backend/src/db/migrations/20240624221840_certificate-alt-names.ts new file mode 100644 index 000000000..fa076b7b4 --- /dev/null +++ b/backend/src/db/migrations/20240624221840_certificate-alt-names.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Certificate)) { + const hasAltNamesColumn = await knex.schema.hasColumn(TableName.Certificate, "altNames"); + if (!hasAltNamesColumn) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.string("altNames").defaultTo(""); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.Certificate)) { + if (await knex.schema.hasColumn(TableName.Certificate, "altNames")) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.dropColumn("altNames"); + }); + } + } +} diff --git a/backend/src/db/migrations/20240626111536_integration-auth-aws-assume-role.ts b/backend/src/db/migrations/20240626111536_integration-auth-aws-assume-role.ts new file mode 100644 index 000000000..0d556a1c0 --- /dev/null +++ b/backend/src/db/migrations/20240626111536_integration-auth-aws-assume-role.ts @@ -0,0 +1,35 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasAwsAssumeRoleCipherText = await knex.schema.hasColumn( + TableName.IntegrationAuth, + "awsAssumeIamRoleArnCipherText" + ); + const hasAwsAssumeRoleIV = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnIV"); + const hasAwsAssumeRoleTag = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnTag"); + if (await knex.schema.hasTable(TableName.IntegrationAuth)) { + await knex.schema.alterTable(TableName.IntegrationAuth, (t) => { + if (!hasAwsAssumeRoleCipherText) t.text("awsAssumeIamRoleArnCipherText"); + if (!hasAwsAssumeRoleIV) t.text("awsAssumeIamRoleArnIV"); + if (!hasAwsAssumeRoleTag) t.text("awsAssumeIamRoleArnTag"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasAwsAssumeRoleCipherText = await knex.schema.hasColumn( + TableName.IntegrationAuth, + "awsAssumeIamRoleArnCipherText" + ); + const hasAwsAssumeRoleIV = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnIV"); + const hasAwsAssumeRoleTag = await knex.schema.hasColumn(TableName.IntegrationAuth, "awsAssumeIamRoleArnTag"); + if (await knex.schema.hasTable(TableName.IntegrationAuth)) { + await knex.schema.alterTable(TableName.IntegrationAuth, (t) => { + if (hasAwsAssumeRoleCipherText) t.dropColumn("awsAssumeIamRoleArnCipherText"); + if (hasAwsAssumeRoleIV) t.dropColumn("awsAssumeIamRoleArnIV"); + if (hasAwsAssumeRoleTag) t.dropColumn("awsAssumeIamRoleArnTag"); + }); + } +} diff --git a/backend/src/db/migrations/20240626115035_admin-login-method-config.ts b/backend/src/db/migrations/20240626115035_admin-login-method-config.ts new file mode 100644 index 000000000..8748fe753 --- /dev/null +++ b/backend/src/db/migrations/20240626115035_admin-login-method-config.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "enabledLoginMethods"))) { + await knex.schema.alterTable(TableName.SuperAdmin, (tb) => { + tb.specificType("enabledLoginMethods", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SuperAdmin, "enabledLoginMethods")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("enabledLoginMethods"); + }); + } +} diff --git a/backend/src/db/migrations/20240626171758_add-ldap-unique-user-attribute.ts b/backend/src/db/migrations/20240626171758_add-ldap-unique-user-attribute.ts new file mode 100644 index 000000000..dc87ff515 --- /dev/null +++ b/backend/src/db/migrations/20240626171758_add-ldap-unique-user-attribute.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.LdapConfig, "uniqueUserAttribute"))) { + await knex.schema.alterTable(TableName.LdapConfig, (tb) => { + tb.string("uniqueUserAttribute").notNullable().defaultTo(""); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.LdapConfig, "uniqueUserAttribute")) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + t.dropColumn("uniqueUserAttribute"); + }); + } +} diff --git a/backend/src/db/migrations/20240626171943_configurable-audit-log-retention.ts b/backend/src/db/migrations/20240626171943_configurable-audit-log-retention.ts new file mode 100644 index 000000000..6ac4b6fe1 --- /dev/null +++ b/backend/src/db/migrations/20240626171943_configurable-audit-log-retention.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Project, "auditLogsRetentionDays"))) { + await knex.schema.alterTable(TableName.Project, (tb) => { + tb.integer("auditLogsRetentionDays").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Project, "auditLogsRetentionDays")) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("auditLogsRetentionDays"); + }); + } +} diff --git a/backend/src/db/migrations/20240627173239_add-oidc-updated-at-trigger.ts b/backend/src/db/migrations/20240627173239_add-oidc-updated-at-trigger.ts new file mode 100644 index 000000000..e0d93d8a2 --- /dev/null +++ b/backend/src/db/migrations/20240627173239_add-oidc-updated-at-trigger.ts @@ -0,0 +1,12 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + await createOnUpdateTrigger(knex, TableName.OidcConfig); +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.OidcConfig); +} diff --git a/backend/src/db/migrations/20240701143900_member-project-favorite.ts b/backend/src/db/migrations/20240701143900_member-project-favorite.ts new file mode 100644 index 000000000..0021cca4c --- /dev/null +++ b/backend/src/db/migrations/20240701143900_member-project-favorite.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.OrgMembership, "projectFavorites"))) { + await knex.schema.alterTable(TableName.OrgMembership, (tb) => { + tb.specificType("projectFavorites", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.OrgMembership, "projectFavorites")) { + await knex.schema.alterTable(TableName.OrgMembership, (t) => { + t.dropColumn("projectFavorites"); + }); + } +} diff --git a/backend/src/db/migrations/20240702055253_add-encrypted-webhook-url.ts b/backend/src/db/migrations/20240702055253_add-encrypted-webhook-url.ts new file mode 100644 index 000000000..8762dde10 --- /dev/null +++ b/backend/src/db/migrations/20240702055253_add-encrypted-webhook-url.ts @@ -0,0 +1,53 @@ +import { Knex } from "knex"; + +import { WebhookType } from "@app/services/webhook/webhook-types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText"); + const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV"); + const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag"); + const hasType = await knex.schema.hasColumn(TableName.Webhook, "type"); + + if (await knex.schema.hasTable(TableName.Webhook)) { + await knex.schema.alterTable(TableName.Webhook, (tb) => { + if (!hasUrlCipherText) { + tb.text("urlCipherText"); + } + if (!hasUrlIV) { + tb.string("urlIV"); + } + if (!hasUrlTag) { + tb.string("urlTag"); + } + if (!hasType) { + tb.string("type").defaultTo(WebhookType.GENERAL); + } + }); + } +} + +export async function down(knex: Knex): Promise { + const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText"); + const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV"); + const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag"); + const hasType = await knex.schema.hasColumn(TableName.Webhook, "type"); + + if (await knex.schema.hasTable(TableName.Webhook)) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (hasUrlCipherText) { + t.dropColumn("urlCipherText"); + } + if (hasUrlIV) { + t.dropColumn("urlIV"); + } + if (hasUrlTag) { + t.dropColumn("urlTag"); + } + if (hasType) { + t.dropColumn("type"); + } + }); + } +} diff --git a/backend/src/db/migrations/20240702131735_secret-approval-groups.ts b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts new file mode 100644 index 000000000..537230b47 --- /dev/null +++ b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts @@ -0,0 +1,188 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + // migrate secret approval policy approvers to user id + const hasApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId"); + const hasApproverId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverId"); + if (!hasApproverUserId) { + // add the new fields + await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { + // if (hasApproverId) tb.setNullable("approverId"); + tb.uuid("approverUserId"); + tb.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + }); + + // convert project membership id => user id + await knex(TableName.SecretApprovalPolicyApprover).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + approverUserId: knex(TableName.ProjectMembership) + .select("userId") + .where("id", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverId`])) + }); + // drop the old field + await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { + if (hasApproverId) tb.dropColumn("approverId"); + tb.uuid("approverUserId").notNullable().alter(); + }); + } + + // migrate secret approval request committer and statusChangeBy to user id + const hasSecretApprovalRequestTable = await knex.schema.hasTable(TableName.SecretApprovalRequest); + const hasCommitterUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId"); + const hasCommitterId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerId"); + const hasStatusChangeBy = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeBy"); + const hasStatusChangedByUserId = await knex.schema.hasColumn( + TableName.SecretApprovalRequest, + "statusChangedByUserId" + ); + if (hasSecretApprovalRequestTable) { + // new fields + await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => { + // if (hasCommitterId) tb.setNullable("committerId"); + if (!hasCommitterUserId) { + tb.uuid("committerUserId"); + tb.foreign("committerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + } + if (!hasStatusChangedByUserId) { + tb.uuid("statusChangedByUserId"); + tb.foreign("statusChangedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + } + }); + + // copy the assigned project membership => user id to new fields + await knex(TableName.SecretApprovalRequest).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + committerUserId: knex(TableName.ProjectMembership) + .select("userId") + .where("id", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerId`])), + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + statusChangedByUserId: knex(TableName.ProjectMembership) + .select("userId") + .where("id", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangeBy`])) + }); + // drop old fields + await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => { + if (hasStatusChangeBy) tb.dropColumn("statusChangeBy"); + if (hasCommitterId) tb.dropColumn("committerId"); + tb.uuid("committerUserId").notNullable().alter(); + }); + } + + // migrate secret approval request reviewer to user id + const hasMemberId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "member"); + const hasReviewerUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "reviewerUserId"); + if (!hasReviewerUserId) { + // new fields + await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => { + // if (hasMemberId) tb.setNullable("member"); + tb.uuid("reviewerUserId"); + tb.foreign("reviewerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + }); + // copy project membership => user id to new fields + await knex(TableName.SecretApprovalRequestReviewer).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + reviewerUserId: knex(TableName.ProjectMembership) + .select("userId") + .where("id", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.member`])) + }); + // drop table + await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => { + if (hasMemberId) tb.dropColumn("member"); + tb.uuid("reviewerUserId").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId"); + const hasApproverId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverId"); + if (hasApproverUserId) { + await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { + if (!hasApproverId) { + tb.uuid("approverId"); + tb.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + } + }); + + if (!hasApproverId) { + await knex(TableName.SecretApprovalPolicyApprover).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + approverId: knex(TableName.ProjectMembership) + .select("id") + .where("userId", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverUserId`])) + }); + await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { + tb.dropColumn("approverUserId"); + tb.uuid("approverId").notNullable().alter(); + }); + } + } + + const hasSecretApprovalRequestTable = await knex.schema.hasTable(TableName.SecretApprovalRequest); + const hasCommitterUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId"); + const hasCommitterId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerId"); + const hasStatusChangeBy = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeBy"); + const hasStatusChangedByUser = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangedByUserId"); + if (hasSecretApprovalRequestTable) { + await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => { + // if (hasCommitterId) tb.uuid("committerId").notNullable().alter(); + if (!hasCommitterId) { + tb.uuid("committerId"); + tb.foreign("committerId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + } + if (!hasStatusChangeBy) { + tb.uuid("statusChangeBy"); + tb.foreign("statusChangeBy").references("id").inTable(TableName.ProjectMembership).onDelete("SET NULL"); + } + }); + + await knex(TableName.SecretApprovalRequest).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + committerId: knex(TableName.ProjectMembership) + .select("id") + .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])), + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + statusChangeBy: knex(TableName.ProjectMembership) + .select("id") + .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangedByUserId`])) + }); + + await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => { + if (hasCommitterUserId) tb.dropColumn("committerUserId"); + if (hasStatusChangedByUser) tb.dropColumn("statusChangedByUserId"); + if (hasCommitterId) tb.uuid("committerId").notNullable().alter(); + }); + } + + const hasMemberId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "member"); + const hasReviewerUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "reviewerUserId"); + if (hasReviewerUserId) { + if (!hasMemberId) { + await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => { + // if (hasMemberId) tb.uuid("member").notNullable().alter(); + tb.uuid("member"); + tb.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + }); + } + await knex(TableName.SecretApprovalRequestReviewer).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + member: knex(TableName.ProjectMembership) + .select("id") + .where("userId", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`])) + }); + await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => { + tb.uuid("member").notNullable().alter(); + tb.dropColumn("reviewerUserId"); + }); + } +} diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index b635420d5..833396fb1 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -19,7 +19,8 @@ export const CertificatesSchema = z.object({ notBefore: z.date(), notAfter: z.date(), revokedAt: z.date().nullable().optional(), - revocationReason: z.number().nullable().optional() + revocationReason: z.number().nullable().optional(), + altNames: z.string().default("").nullable().optional() }); export type TCertificates = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index df126b7f0..af8c2070a 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -43,6 +43,7 @@ export * from "./kms-root-config"; export * from "./ldap-configs"; export * from "./ldap-group-maps"; export * from "./models"; +export * from "./oidc-configs"; export * from "./org-bots"; export * from "./org-memberships"; export * from "./org-roles"; diff --git a/backend/src/db/schemas/integration-auths.ts b/backend/src/db/schemas/integration-auths.ts index 185beae36..0a980edc8 100644 --- a/backend/src/db/schemas/integration-auths.ts +++ b/backend/src/db/schemas/integration-auths.ts @@ -29,7 +29,10 @@ export const IntegrationAuthsSchema = z.object({ keyEncoding: z.string(), projectId: z.string(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + awsAssumeIamRoleArnCipherText: z.string().nullable().optional(), + awsAssumeIamRoleArnIV: z.string().nullable().optional(), + awsAssumeIamRoleArnTag: z.string().nullable().optional() }); export type TIntegrationAuths = z.infer; diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 86fd6acb6..460c2cff6 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -26,7 +26,8 @@ export const LdapConfigsSchema = z.object({ updatedAt: z.date(), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), - searchFilter: z.string().default("") + searchFilter: z.string().default(""), + uniqueUserAttribute: z.string().default("") }); export type TLdapConfigs = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index d7a5e6de1..bdc574bcb 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -78,6 +78,7 @@ export enum TableName { SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", LdapConfig = "ldap_configs", + OidcConfig = "oidc_configs", LdapGroupMap = "ldap_group_maps", AuditLog = "audit_logs", AuditLogStream = "audit_log_streams", diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts new file mode 100644 index 000000000..d78f9e9a7 --- /dev/null +++ b/backend/src/db/schemas/oidc-configs.ts @@ -0,0 +1,34 @@ +// 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 OidcConfigsSchema = z.object({ + id: z.string().uuid(), + discoveryURL: z.string().nullable().optional(), + issuer: z.string().nullable().optional(), + authorizationEndpoint: z.string().nullable().optional(), + jwksUri: z.string().nullable().optional(), + tokenEndpoint: z.string().nullable().optional(), + userinfoEndpoint: z.string().nullable().optional(), + encryptedClientId: z.string(), + configurationType: z.string(), + clientIdIV: z.string(), + clientIdTag: z.string(), + encryptedClientSecret: z.string(), + clientSecretIV: z.string(), + clientSecretTag: z.string(), + allowedEmailDomains: z.string().nullable().optional(), + isActive: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid() +}); + +export type TOidcConfigs = z.infer; +export type TOidcConfigsInsert = Omit, TImmutableDBKeys>; +export type TOidcConfigsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/org-memberships.ts b/backend/src/db/schemas/org-memberships.ts index 585addb7c..b1858e5be 100644 --- a/backend/src/db/schemas/org-memberships.ts +++ b/backend/src/db/schemas/org-memberships.ts @@ -16,7 +16,8 @@ export const OrgMembershipsSchema = z.object({ updatedAt: z.date(), userId: z.string().uuid().nullable().optional(), orgId: z.string().uuid(), - roleId: z.string().uuid().nullable().optional() + roleId: z.string().uuid().nullable().optional(), + projectFavorites: z.string().array().nullable().optional() }); export type TOrgMemberships = z.infer; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 91035ab8e..f776e864c 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -17,8 +17,9 @@ export const ProjectsSchema = z.object({ updatedAt: z.date(), version: z.number().default(1), upgradeStatus: z.string().nullable().optional(), + pitVersionLimit: z.number().default(10), kmsCertificateKeyId: z.string().uuid().nullable().optional(), - pitVersionLimit: z.number().default(10) + auditLogsRetentionDays: z.number().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/db/schemas/secret-approval-policies-approvers.ts b/backend/src/db/schemas/secret-approval-policies-approvers.ts index 12a3119e6..3af5614f4 100644 --- a/backend/src/db/schemas/secret-approval-policies-approvers.ts +++ b/backend/src/db/schemas/secret-approval-policies-approvers.ts @@ -9,10 +9,10 @@ import { TImmutableDBKeys } from "./models"; export const SecretApprovalPoliciesApproversSchema = z.object({ id: z.string().uuid(), - approverId: z.string().uuid(), policyId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + approverUserId: z.string().uuid() }); export type TSecretApprovalPoliciesApprovers = z.infer; diff --git a/backend/src/db/schemas/secret-approval-requests-reviewers.ts b/backend/src/db/schemas/secret-approval-requests-reviewers.ts index f3ff88047..a5c445587 100644 --- a/backend/src/db/schemas/secret-approval-requests-reviewers.ts +++ b/backend/src/db/schemas/secret-approval-requests-reviewers.ts @@ -9,11 +9,11 @@ import { TImmutableDBKeys } from "./models"; export const SecretApprovalRequestsReviewersSchema = z.object({ id: z.string().uuid(), - member: z.string().uuid(), status: z.string(), requestId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + reviewerUserId: z.string().uuid() }); export type TSecretApprovalRequestsReviewers = z.infer; diff --git a/backend/src/db/schemas/secret-approval-requests.ts b/backend/src/db/schemas/secret-approval-requests.ts index 77ad370b7..aa6896d10 100644 --- a/backend/src/db/schemas/secret-approval-requests.ts +++ b/backend/src/db/schemas/secret-approval-requests.ts @@ -15,11 +15,11 @@ export const SecretApprovalRequestsSchema = z.object({ conflicts: z.unknown().nullable().optional(), slug: z.string(), folderId: z.string().uuid(), - statusChangeBy: z.string().uuid().nullable().optional(), - committerId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - isReplicated: z.boolean().nullable().optional() + isReplicated: z.boolean().nullable().optional(), + committerUserId: z.string().uuid(), + statusChangedByUserId: z.string().uuid().nullable().optional() }); export type TSecretApprovalRequests = z.infer; diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 417d4e05e..b676b81a8 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -16,7 +16,10 @@ export const SuperAdminSchema = z.object({ allowedSignUpDomain: z.string().nullable().optional(), instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"), trustSamlEmails: z.boolean().default(false).nullable().optional(), - trustLdapEmails: z.boolean().default(false).nullable().optional() + trustLdapEmails: z.boolean().default(false).nullable().optional(), + trustOidcEmails: z.boolean().default(false).nullable().optional(), + defaultAuthOrgId: z.string().uuid().nullable().optional(), + enabledLoginMethods: z.string().array().nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 44aa8c5da..a7aac2933 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -21,7 +21,11 @@ export const WebhooksSchema = z.object({ keyEncoding: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - envId: z.string().uuid() + envId: z.string().uuid(), + urlCipherText: z.string().nullable().optional(), + urlIV: z.string().nullable().optional(), + urlTag: z.string().nullable().optional(), + type: z.string().default("general").nullable().optional() }); export type TWebhooks = z.infer; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index d04bd86fd..6bd9176d8 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -8,6 +8,7 @@ import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; +import { registerOidcRouter } from "./oidc-router"; import { registerOrgRoleRouter } from "./org-role-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRouter } from "./project-router"; @@ -64,7 +65,14 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { { prefix: "/pki" } ); - await server.register(registerSamlRouter, { prefix: "/sso" }); + await server.register( + async (ssoRouter) => { + await ssoRouter.register(registerSamlRouter); + await ssoRouter.register(registerOidcRouter, { prefix: "/oidc" }); + }, + { prefix: "/sso" } + ); + await server.register(registerScimRouter, { prefix: "/scim" }); await server.register(registerLdapRouter, { prefix: "/ldap" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 8cccbaac6..735ba632c 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -70,10 +70,13 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase); } + const externalId = ldapConfig.uniqueUserAttribute ? user[ldapConfig.uniqueUserAttribute] : user.uidNumber; + const username = ldapConfig.uniqueUserAttribute ? externalId : user.uid; + const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({ + externalId, + username, ldapConfigId: ldapConfig.id, - externalId: user.uidNumber, - username: user.uid, firstName: user.givenName ?? user.cn ?? "", lastName: user.sn ?? "", email: user.mail, @@ -138,6 +141,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { url: z.string(), bindDN: z.string(), bindPass: z.string(), + uniqueUserAttribute: z.string(), searchBase: z.string(), searchFilter: z.string(), groupSearchBase: z.string(), @@ -172,6 +176,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { url: z.string().trim(), bindDN: z.string().trim(), bindPass: z.string().trim(), + uniqueUserAttribute: z.string().trim().default("uidNumber"), searchBase: z.string().trim(), searchFilter: z.string().trim().default("(uid={{username}})"), groupSearchBase: z.string().trim(), @@ -213,6 +218,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { url: z.string().trim(), bindDN: z.string().trim(), bindPass: z.string().trim(), + uniqueUserAttribute: z.string().trim(), searchBase: z.string().trim(), searchFilter: z.string().trim(), groupSearchBase: z.string().trim(), diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts new file mode 100644 index 000000000..e675121e9 --- /dev/null +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -0,0 +1,355 @@ +/* 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 { Authenticator, Strategy } from "@fastify/passport"; +import fastifySession from "@fastify/session"; +import RedisStore from "connect-redis"; +import { Redis } from "ioredis"; +import { z } from "zod"; + +import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs"; +import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; +import { getConfig } from "@app/lib/config/env"; +import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerOidcRouter = async (server: FastifyZodProvider) => { + const appCfg = getConfig(); + const redis = new Redis(appCfg.REDIS_URL); + const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" }); + + /* + - OIDC protocol cannot work without sessions: https://github.com/panva/node-openid-client/issues/190 + - Current redis usage is not ideal and will eventually have to be refactored to use a better structure + - Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js + */ + const redisStore = new RedisStore({ + client: redis, + prefix: "oidc-session:", + ttl: 600 // 10 minutes + }); + + await server.register(fastifySession, { + secret: appCfg.COOKIE_SECRET_SIGN_KEY, + store: redisStore, + cookie: { + secure: appCfg.HTTPS_ENABLED, + sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server + } + }); + + await server.register(passport.initialize()); + await server.register(passport.secureSession()); + + // redirect to IDP for login + server.route({ + url: "/login", + method: "GET", + config: { + rateLimit: authRateLimit + }, + schema: { + querystring: z.object({ + orgSlug: z.string().trim(), + callbackPort: z.string().trim().optional() + }) + }, + preValidation: [ + async (req, res) => { + const { orgSlug, callbackPort } = req.query; + + // ensure fresh session state per login attempt + await req.session.regenerate(); + + req.session.set("oidcOrgSlug", orgSlug); + + if (callbackPort) { + req.session.set("callbackPort", callbackPort); + } + + const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(orgSlug, callbackPort); + return ( + passport.authenticate(oidcStrategy as Strategy, { + scope: "profile email openid" + }) as any + )(req, res); + } + ], + handler: () => {} + }); + + // callback route after login from IDP + server.route({ + url: "/callback", + method: "GET", + preValidation: [ + async (req, res) => { + const oidcOrgSlug = req.session.get("oidcOrgSlug"); + const callbackPort = req.session.get("callbackPort"); + const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(oidcOrgSlug, callbackPort); + + return ( + passport.authenticate(oidcStrategy as Strategy, { + failureRedirect: "/api/v1/sso/oidc/login/error", + session: false, + failureMessage: true + }) as any + )(req, res); + } + ], + handler: async (req, res) => { + await req.session.destroy(); + + if (req.passportUser.isUserCompleted) { + return res.redirect( + `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` + ); + } + + // signup + return res.redirect( + `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` + ); + } + }); + + server.route({ + url: "/login/error", + method: "GET", + handler: async (req, res) => { + await req.session.destroy(); + + return res.status(500).send({ + error: "Authentication error", + details: req.query + }); + } + }); + + server.route({ + url: "/config", + method: "GET", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + querystring: z.object({ + orgSlug: z.string().trim() + }), + response: { + 200: OidcConfigsSchema.pick({ + id: true, + issuer: true, + authorizationEndpoint: true, + jwksUri: true, + tokenEndpoint: true, + userinfoEndpoint: true, + configurationType: true, + discoveryURL: true, + isActive: true, + orgId: true, + allowedEmailDomains: true + }).extend({ + clientId: z.string(), + clientSecret: z.string() + }) + } + }, + handler: async (req) => { + const { orgSlug } = req.query; + const oidc = await server.services.oidc.getOidc({ + orgSlug, + type: "external", + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return oidc; + } + }); + + server.route({ + method: "PATCH", + url: "/config", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z + .object({ + allowedEmailDomains: z + .string() + .trim() + .optional() + .default("") + .transform((data) => { + if (data === "") return ""; + // Trim each ID and join with ', ' to ensure formatting + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }), + discoveryURL: z.string().trim(), + configurationType: z.nativeEnum(OIDCConfigurationType), + issuer: z.string().trim(), + authorizationEndpoint: z.string().trim(), + jwksUri: z.string().trim(), + tokenEndpoint: z.string().trim(), + userinfoEndpoint: z.string().trim(), + clientId: z.string().trim(), + clientSecret: z.string().trim(), + isActive: z.boolean() + }) + .partial() + .merge(z.object({ orgSlug: z.string() })), + response: { + 200: OidcConfigsSchema.pick({ + id: true, + issuer: true, + authorizationEndpoint: true, + configurationType: true, + discoveryURL: true, + jwksUri: true, + tokenEndpoint: true, + userinfoEndpoint: true, + orgId: true, + allowedEmailDomains: true, + isActive: true + }) + } + }, + handler: async (req) => { + const oidc = await server.services.oidc.updateOidcCfg({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + return oidc; + } + }); + + server.route({ + method: "POST", + url: "/config", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + body: z + .object({ + allowedEmailDomains: z + .string() + .trim() + .optional() + .default("") + .transform((data) => { + if (data === "") return ""; + // Trim each ID and join with ', ' to ensure formatting + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }), + configurationType: z.nativeEnum(OIDCConfigurationType), + issuer: z.string().trim().optional().default(""), + discoveryURL: z.string().trim().optional().default(""), + authorizationEndpoint: z.string().trim().optional().default(""), + jwksUri: z.string().trim().optional().default(""), + tokenEndpoint: z.string().trim().optional().default(""), + userinfoEndpoint: z.string().trim().optional().default(""), + clientId: z.string().trim(), + clientSecret: z.string().trim(), + isActive: z.boolean(), + orgSlug: z.string().trim() + }) + .superRefine((data, ctx) => { + if (data.configurationType === OIDCConfigurationType.CUSTOM) { + if (!data.issuer) { + ctx.addIssue({ + path: ["issuer"], + message: "Issuer is required", + code: z.ZodIssueCode.custom + }); + } + if (!data.authorizationEndpoint) { + ctx.addIssue({ + path: ["authorizationEndpoint"], + message: "Authorization endpoint is required", + code: z.ZodIssueCode.custom + }); + } + if (!data.jwksUri) { + ctx.addIssue({ + path: ["jwksUri"], + message: "JWKS URI is required", + code: z.ZodIssueCode.custom + }); + } + if (!data.tokenEndpoint) { + ctx.addIssue({ + path: ["tokenEndpoint"], + message: "Token endpoint is required", + code: z.ZodIssueCode.custom + }); + } + if (!data.userinfoEndpoint) { + ctx.addIssue({ + path: ["userinfoEndpoint"], + message: "Userinfo endpoint is required", + code: z.ZodIssueCode.custom + }); + } + } else { + // eslint-disable-next-line no-lonely-if + if (!data.discoveryURL) { + ctx.addIssue({ + path: ["discoveryURL"], + message: "Discovery URL is required", + code: z.ZodIssueCode.custom + }); + } + } + }), + response: { + 200: OidcConfigsSchema.pick({ + id: true, + issuer: true, + authorizationEndpoint: true, + configurationType: true, + discoveryURL: true, + jwksUri: true, + tokenEndpoint: true, + userinfoEndpoint: true, + orgId: true, + isActive: true, + allowedEmailDomains: true + }) + } + }, + + handler: async (req) => { + const oidc = await server.services.oidc.createOidcCfg({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + return oidc; + } + }); +}; diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index b09b58e26..ee10131dd 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -25,10 +25,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .optional() .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)), - approvers: z.string().array().min(1), + approverUserIds: z.string().array().min(1), approvals: z.number().min(1).default(1) }) - .refine((data) => data.approvals <= data.approvers.length, { + .refine((data) => data.approvals <= data.approverUserIds.length, { path: ["approvals"], message: "The number of approvals should be lower than the number of approvers." }), @@ -66,7 +66,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi body: z .object({ name: z.string().optional(), - approvers: z.string().array().min(1), + approverUserIds: z.string().array().min(1), approvals: z.number().min(1).default(1), secretPath: z .string() @@ -74,7 +74,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)) }) - .refine((data) => data.approvals <= data.approvers.length, { + .refine((data) => data.approvals <= data.approverUserIds.length, { path: ["approvals"], message: "The number of approvals should be lower than the number of approvers." }), @@ -139,7 +139,15 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi }), response: { 200: z.object({ - approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array() + approvals: sapPubSchema + .extend({ + userApprovers: z + .object({ + userId: z.string() + }) + .array() + }) + .array() }) } }, @@ -170,7 +178,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi }), response: { 200: z.object({ - policy: sapPubSchema.merge(z.object({ approvers: z.string().array() })).optional() + policy: sapPubSchema + .extend({ + userApprovers: z.object({ userId: z.string() }).array() + }) + .optional() }) } }, diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index b7204f72e..8e72597bd 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -6,7 +6,8 @@ import { SecretApprovalRequestsSecretsSchema, SecretsSchema, SecretTagsSchema, - SecretVersionsSchema + SecretVersionsSchema, + UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; @@ -14,6 +15,15 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const approvalRequestUser = z.object({ userId: z.string() }).merge( + UsersSchema.pick({ + email: true, + firstName: true, + lastName: true, + username: true + }) +); + export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", @@ -41,9 +51,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv approvers: z.string().array(), secretPath: z.string().optional().nullable() }), + committerUser: approvalRequestUser, commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), environment: z.string(), - reviewers: z.object({ member: z.string(), status: z.string() }).array(), + reviewers: z.object({ userId: z.string(), status: z.string() }).array(), approvers: z.string().array() }).array() }) @@ -195,7 +206,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED, // eslint-disable-next-line metadata: { - [isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangeBy as string, + [isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangedByUserId as string, secretApprovalRequestId: approval.id, secretApprovalRequestSlug: approval.slug // eslint-disable-next-line @@ -216,6 +227,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }) .array() .optional(); + server.route({ method: "GET", url: "/:id", @@ -235,12 +247,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv id: z.string(), name: z.string(), approvals: z.number(), - approvers: z.string().array(), + approvers: approvalRequestUser.array(), secretPath: z.string().optional().nullable() }), environment: z.string(), - reviewers: z.object({ member: z.string(), status: z.string() }).array(), - approvers: z.string().array(), + statusChangedByUser: approvalRequestUser.optional(), + committerUser: approvalRequestUser, + reviewers: approvalRequestUser.extend({ status: z.string() }).array(), secretPath: z.string(), commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true }) .merge( diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 88e288832..77ae430c6 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -32,7 +32,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await accessApprovalPolicyFindQuery(tx || db, { + const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), { [`${TableName.AccessApprovalPolicy}.id` as "id"]: id }); const formatedDoc = mergeOneToManyRelation( @@ -54,7 +54,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const find = async (filter: TFindFilter, tx?: Knex) => { try { - const docs = await accessApprovalPolicyFindQuery(tx || db, filter); + const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter); const formatedDoc = mergeOneToManyRelation( docs, "id", diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index c3f4c72a6..c3c0d24d0 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -14,7 +14,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => { try { - const docs = await db(TableName.AccessApprovalRequest) + const docs = await db + .replicaNode()(TableName.AccessApprovalRequest) .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) .leftJoin( @@ -170,7 +171,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db); + const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode()); const docs = await sql; const formatedDoc = sqlNestRelationships({ data: docs, @@ -207,7 +208,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { const getCount = async ({ projectId }: { projectId: string }) => { try { - const accessRequests = await db(TableName.AccessApprovalRequest) + const accessRequests = await db + .replicaNode()(TableName.AccessApprovalRequest) .leftJoin( TableName.AccessApprovalPolicy, `${TableName.AccessApprovalRequest}.policyId`, diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index b3ad8c2b6..316cf34a5 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, stripUndefinedInWhere } from "@app/lib/knex"; +import { logger } from "@app/lib/logger"; export type TAuditLogDALFactory = ReturnType; @@ -27,7 +28,7 @@ export const auditLogDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const sqlQuery = (tx || db)(TableName.AuditLog) + const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog) .where( stripUndefinedInWhere({ projectId, @@ -55,13 +56,34 @@ export const auditLogDALFactory = (db: TDbClient) => { // delete all audit log that have expired const pruneAuditLog = async (tx?: Knex) => { - try { - const today = new Date(); - const docs = await (tx || db)(TableName.AuditLog).where("expiresAt", "<", today).del(); - return docs; - } catch (error) { - throw new DatabaseError({ error, name: "PruneAuditLog" }); - } + const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000; + const MAX_RETRY_ON_FAILURE = 3; + + const today = new Date(); + let deletedAuditLogIds: { id: string }[] = []; + let numberOfRetryOnFailure = 0; + + do { + try { + const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog) + .where("expiresAt", "<", today) + .select("id") + .limit(AUDIT_LOG_PRUNE_BATCH_SIZE); + // eslint-disable-next-line no-await-in-loop + deletedAuditLogIds = await (tx || db)(TableName.AuditLog) + .whereIn("id", findExpiredLogSubQuery) + .del() + .returning("id"); + numberOfRetryOnFailure = 0; // reset + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 100); // time to breathe for db + }); + } catch (error) { + numberOfRetryOnFailure += 1; + logger.error(error, "Failed to delete audit log on pruning"); + } + } while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE); }; return { ...auditLogOrm, pruneAuditLog, find }; 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 f93b391a5..3fde40c8e 100644 --- a/backend/src/ee/services/audit-log/audit-log-queue.ts +++ b/backend/src/ee/services/audit-log/audit-log-queue.ts @@ -45,18 +45,29 @@ export const auditLogQueueServiceFactory = ({ const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data; let { orgId } = job.data; const MS_IN_DAY = 24 * 60 * 60 * 1000; + let project; if (!orgId) { // it will never be undefined for both org and project id // TODO(akhilmhdh): use caching here in dal to avoid db calls - const project = await projectDAL.findById(projectId as string); + project = await projectDAL.findById(projectId as string); orgId = project.orgId; } const plan = await licenseService.getPlan(orgId); - const ttl = plan.auditLogsRetentionDays * MS_IN_DAY; - // skip inserting if audit log retention is 0 meaning its not supported - if (ttl === 0) return; + if (plan.auditLogsRetentionDays === 0) { + // skip inserting if audit log retention is 0 meaning its not supported + return; + } + + // For project actions, set TTL to project-level audit log retention config + // This condition ensures that the plan's audit log retention days cannot be bypassed + const ttlInDays = + project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays + ? project.auditLogsRetentionDays + : plan.auditLogsRetentionDays; + + const ttl = ttlInDays * MS_IN_DAY; const auditLog = await auditLogDAL.create({ actor: actor.type, 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 8a0d2aef9..18930cfe8 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -65,25 +65,31 @@ export enum EventType { ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth", UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth", + REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth", LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth", ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth", UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth", GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth", + REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth", CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", + GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id", LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth", ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth", UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth", + REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth", GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth", LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth", ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth", UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth", + REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth", GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth", LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth", ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth", UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth", GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth", + REVOKE_IDENTITY_AZURE_AUTH = "revoke-identity-azure-auth", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", @@ -434,6 +440,13 @@ interface GetIdentityUniversalAuthEvent { }; } +interface DeleteIdentityUniversalAuthEvent { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + }; +} + interface LoginIdentityKubernetesAuthEvent { type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH; metadata: { @@ -457,6 +470,13 @@ interface AddIdentityKubernetesAuthEvent { }; } +interface DeleteIdentityKubernetesAuthEvent { + type: EventType.REVOKE_IDENTITY_KUBERNETES_AUTH; + metadata: { + identityId: string; + }; +} + interface UpdateIdentityKubernetesAuthEvent { type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH; metadata: { @@ -493,6 +513,14 @@ interface GetIdentityUniversalAuthClientSecretsEvent { }; } +interface GetIdentityUniversalAuthClientSecretByIdEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID; + metadata: { + identityId: string; + clientSecretId: string; + }; +} + interface RevokeIdentityUniversalAuthClientSecretEvent { type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET; metadata: { @@ -525,6 +553,13 @@ interface AddIdentityGcpAuthEvent { }; } +interface DeleteIdentityGcpAuthEvent { + type: EventType.REVOKE_IDENTITY_GCP_AUTH; + metadata: { + identityId: string; + }; +} + interface UpdateIdentityGcpAuthEvent { type: EventType.UPDATE_IDENTITY_GCP_AUTH; metadata: { @@ -570,6 +605,13 @@ interface AddIdentityAwsAuthEvent { }; } +interface DeleteIdentityAwsAuthEvent { + type: EventType.REVOKE_IDENTITY_AWS_AUTH; + metadata: { + identityId: string; + }; +} + interface UpdateIdentityAwsAuthEvent { type: EventType.UPDATE_IDENTITY_AWS_AUTH; metadata: { @@ -613,6 +655,13 @@ interface AddIdentityAzureAuthEvent { }; } +interface DeleteIdentityAzureAuthEvent { + type: EventType.REVOKE_IDENTITY_AZURE_AUTH; + metadata: { + identityId: string; + }; +} + interface UpdateIdentityAzureAuthEvent { type: EventType.UPDATE_IDENTITY_AZURE_AUTH; metadata: { @@ -722,7 +771,6 @@ interface CreateWebhookEvent { webhookId: string; environment: string; secretPath: string; - webhookUrl: string; isDisabled: boolean; }; } @@ -733,7 +781,6 @@ interface UpdateWebhookStatusEvent { webhookId: string; environment: string; secretPath: string; - webhookUrl: string; isDisabled: boolean; }; } @@ -744,7 +791,6 @@ interface DeleteWebhookEvent { webhookId: string; environment: string; secretPath: string; - webhookUrl: string; isDisabled: boolean; }; } @@ -1003,24 +1049,30 @@ export type Event = | LoginIdentityUniversalAuthEvent | AddIdentityUniversalAuthEvent | UpdateIdentityUniversalAuthEvent + | DeleteIdentityUniversalAuthEvent | GetIdentityUniversalAuthEvent | LoginIdentityKubernetesAuthEvent + | DeleteIdentityKubernetesAuthEvent | AddIdentityKubernetesAuthEvent | UpdateIdentityKubernetesAuthEvent | GetIdentityKubernetesAuthEvent | CreateIdentityUniversalAuthClientSecretEvent | GetIdentityUniversalAuthClientSecretsEvent + | GetIdentityUniversalAuthClientSecretByIdEvent | RevokeIdentityUniversalAuthClientSecretEvent | LoginIdentityGcpAuthEvent | AddIdentityGcpAuthEvent + | DeleteIdentityGcpAuthEvent | UpdateIdentityGcpAuthEvent | GetIdentityGcpAuthEvent | LoginIdentityAwsAuthEvent | AddIdentityAwsAuthEvent | UpdateIdentityAwsAuthEvent | GetIdentityAwsAuthEvent + | DeleteIdentityAwsAuthEvent | LoginIdentityAzureAuthEvent | AddIdentityAzureAuthEvent + | DeleteIdentityAzureAuthEvent | UpdateIdentityAzureAuthEvent | GetIdentityAzureAuthEvent | CreateEnvironmentEvent diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 810628030..339b2d626 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -12,7 +12,10 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first(); + const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) + .count("*") + .where({ dynamicSecretId }) + .first(); return parseInt(doc || "0", 10); } catch (error) { throw new DatabaseError({ error, name: "DynamicSecretCountLeases" }); @@ -21,7 +24,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.DynamicSecretLease) + const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id }) .first() .join( diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index c11f6ddfb..14b79eeea 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -3,7 +3,8 @@ import { z } from "zod"; export enum SqlProviders { Postgres = "postgres", MySQL = "mysql2", - Oracle = "oracledb" + Oracle = "oracledb", + MsSQL = "mssql" } export const DynamicSecretSqlDBSchema = z.object({ diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts index 3da1f242c..4f8ffa664 100644 --- a/backend/src/ee/services/group/group-dal.ts +++ b/backend/src/ee/services/group/group-dal.ts @@ -12,7 +12,7 @@ export const groupDALFactory = (db: TDbClient) => { const findGroups = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { try { - const query = (tx || db)(TableName.Groups) + const query = (tx || db.replicaNode())(TableName.Groups) // eslint-disable-next-line .where(buildFindFilter(filter)) .select(selectAllTableCols(TableName.Groups)); @@ -32,7 +32,7 @@ export const groupDALFactory = (db: TDbClient) => { const findByOrgId = async (orgId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.Groups) + const docs = await (tx || db.replicaNode())(TableName.Groups) .where(`${TableName.Groups}.orgId`, orgId) .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) .select(selectAllTableCols(TableName.Groups)) @@ -74,11 +74,12 @@ export const groupDALFactory = (db: TDbClient) => { username?: string; }) => { try { - let query = db(TableName.OrgMembership) + let query = db + .replicaNode()(TableName.OrgMembership) .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.UserGroupMembership, function () { - this.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn( + .leftJoin(TableName.UserGroupMembership, (bd) => { + bd.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn( `${TableName.UserGroupMembership}.groupId`, "=", db.raw("?", [groupId]) diff --git a/backend/src/ee/services/group/user-group-membership-dal.ts b/backend/src/ee/services/group/user-group-membership-dal.ts index 1ab1839c5..e20cf317b 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -18,7 +18,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { */ const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[], tx?: Knex) => { try { - const userProjectMemberships: string[] = await (tx || db)(TableName.ProjectMembership) + const userProjectMemberships: string[] = await (tx || db.replicaNode())(TableName.ProjectMembership) .where(`${TableName.ProjectMembership}.userId`, userId) .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds) .pluck(`${TableName.ProjectMembership}.projectId`); @@ -43,7 +43,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { // special query const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => { try { - const usernameDocs: string[] = await db(TableName.UserGroupMembership) + const usernameDocs: string[] = await db + .replicaNode()(TableName.UserGroupMembership) .join( TableName.GroupProjectMembership, `${TableName.UserGroupMembership}.groupId`, @@ -73,7 +74,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { try { // get list of groups in the project with id [projectId] // that that are not the group with id [groupId] - const groups: string[] = await (tx || db)(TableName.GroupProjectMembership) + const groups: string[] = await (tx || db.replicaNode())(TableName.GroupProjectMembership) .where(`${TableName.GroupProjectMembership}.projectId`, projectId) .whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId) .pluck(`${TableName.GroupProjectMembership}.groupId`); @@ -83,8 +84,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { .where(`${TableName.UserGroupMembership}.groupId`, groupId) .where(`${TableName.UserGroupMembership}.isPending`, false) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.ProjectMembership, function () { - this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn( + .leftJoin(TableName.ProjectMembership, (bd) => { + bd.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn( `${TableName.ProjectMembership}.projectId`, "=", db.raw("?", [projectId]) @@ -107,9 +108,9 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { db.ref("publicKey").withSchema(TableName.UserEncryptionKey) ) .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER - .whereNotIn(`${TableName.UserGroupMembership}.userId`, function () { + .whereNotIn(`${TableName.UserGroupMembership}.userId`, (bd) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.select(`${TableName.UserGroupMembership}.userId`) + bd.select(`${TableName.UserGroupMembership}.userId`) .from(TableName.UserGroupMembership) .whereIn(`${TableName.UserGroupMembership}.groupId`, groups); }); diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 8027f5907..e1b2e011a 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -23,6 +23,8 @@ import { } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; +import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; +import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; @@ -30,7 +32,9 @@ import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membe import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; 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"; @@ -50,7 +54,7 @@ import { TTestLdapConnectionDTO, TUpdateLdapCfgDTO } from "./ldap-config-types"; -import { testLDAPConfig } from "./ldap-fns"; +import { searchGroups, testLDAPConfig } from "./ldap-fns"; import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal"; type TLdapConfigServiceFactoryDep = { @@ -84,6 +88,8 @@ type TLdapConfigServiceFactoryDep = { userAliasDAL: Pick; permissionService: Pick; licenseService: Pick; + tokenService: Pick; + smtpService: Pick; }; export type TLdapConfigServiceFactory = ReturnType; @@ -103,7 +109,9 @@ export const ldapConfigServiceFactory = ({ userDAL, userAliasDAL, permissionService, - licenseService + licenseService, + tokenService, + smtpService }: TLdapConfigServiceFactoryDep) => { const createLdapCfg = async ({ actor, @@ -115,6 +123,7 @@ export const ldapConfigServiceFactory = ({ url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -193,6 +202,7 @@ export const ldapConfigServiceFactory = ({ encryptedBindPass, bindPassIV, bindPassTag, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -215,6 +225,7 @@ export const ldapConfigServiceFactory = ({ url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -237,7 +248,8 @@ export const ldapConfigServiceFactory = ({ searchBase, searchFilter, groupSearchBase, - groupSearchFilter + groupSearchFilter, + uniqueUserAttribute }; const orgBot = await orgBotDAL.findOne({ orgId }); @@ -275,7 +287,7 @@ export const ldapConfigServiceFactory = ({ return ldapConfig; }; - const getLdapCfg = async (filter: { orgId: string; isActive?: boolean }) => { + const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => { const ldapConfig = await ldapConfigDAL.findOne(filter); if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); @@ -338,6 +350,7 @@ export const ldapConfigServiceFactory = ({ url: ldapConfig.url, bindDN, bindPass, + uniqueUserAttribute: ldapConfig.uniqueUserAttribute, searchBase: ldapConfig.searchBase, searchFilter: ldapConfig.searchFilter, groupSearchBase: ldapConfig.groupSearchBase, @@ -374,6 +387,7 @@ export const ldapConfigServiceFactory = ({ url: ldapConfig.url, bindDN: ldapConfig.bindDN, bindCredentials: ldapConfig.bindPass, + uniqueUserAttribute: ldapConfig.uniqueUserAttribute, searchBase: ldapConfig.searchBase, searchFilter: ldapConfig.searchFilter || "(uid={{username}})", // searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"], @@ -404,6 +418,13 @@ export const ldapConfigServiceFactory = ({ }: TLdapLoginDTO) => { const appCfg = getConfig(); const serverCfg = await getServerCfg(); + + if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) { + throw new BadRequestError({ + message: "Login with LDAP is disabled by administrator." + }); + } + let userAlias = await userAliasDAL.findOne({ externalId, orgId, @@ -443,9 +464,24 @@ export const ldapConfigServiceFactory = ({ } }); } else { + const plan = await licenseService.getPlan(orgId); + if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + // limit imposed on number of members allowed / number of members used exceeds the number of members allowed + throw new BadRequestError({ + message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." + }); + } + + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." + }); + } + userAlias = await userDAL.transaction(async (tx) => { let newUser: TUsers | undefined; - if (serverCfg.trustSamlEmails) { + if (serverCfg.trustLdapEmails) { newUser = await userDAL.findOne( { email, @@ -494,7 +530,7 @@ export const ldapConfigServiceFactory = ({ if (!orgMembership) { await orgMembershipDAL.create( { - userId: userAlias.userId, + userId: newUser.id, inviteEmail: email, orgId, role: OrgMembershipRole.Member, @@ -627,6 +663,22 @@ export const ldapConfigServiceFactory = ({ } ); + if (user.email && !user.isEmailVerified) { + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_VERIFICATION, + userId: user.id + }); + + await smtpService.sendMail({ + template: SmtpTemplates.EmailVerification, + subjectLine: "Infisical confirmation code", + recipients: [user.email], + substitutions: { + code: token + } + }); + } + return { isUserCompleted, providerAuthToken }; }; @@ -672,11 +724,25 @@ export const ldapConfigServiceFactory = ({ message: "Failed to create LDAP group map due to plan restriction. Upgrade plan to create LDAP group map." }); - const ldapConfig = await ldapConfigDAL.findOne({ - id: ldapConfigId, - orgId + const ldapConfig = await getLdapCfg({ + orgId, + id: ldapConfigId }); - if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); + + if (!ldapConfig.groupSearchBase) { + throw new BadRequestError({ + message: "Configure a group search base in your LDAP configuration in order to proceed." + }); + } + + const groupSearchFilter = `(cn=${ldapGroupCN})`; + const groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase); + + if (!groups.some((g) => g.cn === ldapGroupCN)) { + throw new BadRequestError({ + message: "Failed to find LDAP Group CN" + }); + } const group = await groupDAL.findOne({ slug: groupSlug, orgId }); if (!group) throw new BadRequestError({ message: "Failed to find group" }); diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts index aa4aa8da7..86f4bf0d5 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-types.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts @@ -7,6 +7,7 @@ export type TLDAPConfig = { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; groupSearchBase: string; groupSearchFilter: string; @@ -19,6 +20,7 @@ export type TCreateLdapCfgDTO = { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; searchFilter: string; groupSearchBase: string; @@ -33,6 +35,7 @@ export type TUpdateLdapCfgDTO = { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; searchFilter: string; groupSearchBase: string; diff --git a/backend/src/ee/services/ldap-config/ldap-group-map-dal.ts b/backend/src/ee/services/ldap-config/ldap-group-map-dal.ts index 2264efa75..a08522e8d 100644 --- a/backend/src/ee/services/ldap-config/ldap-group-map-dal.ts +++ b/backend/src/ee/services/ldap-config/ldap-group-map-dal.ts @@ -10,7 +10,8 @@ export const ldapGroupMapDALFactory = (db: TDbClient) => { const findLdapGroupMapsByLdapConfigId = async (ldapConfigId: string) => { try { - const docs = await db(TableName.LdapGroupMap) + const docs = await db + .replicaNode()(TableName.LdapGroupMap) .where(`${TableName.LdapGroupMap}.ldapConfigId`, ldapConfigId) .join(TableName.Groups, `${TableName.LdapGroupMap}.groupId`, `${TableName.Groups}.id`) .select(selectAllTableCols(TableName.LdapGroupMap)) diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts index ddbffba45..a8b3b351d 100644 --- a/backend/src/ee/services/license/__mocks__/licence-fns.ts +++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts @@ -7,6 +7,8 @@ export const getDefaultOnPremFeatures = () => { workspacesUsed: 0, memberLimit: null, membersUsed: 0, + identityLimit: null, + identitiesUsed: 0, environmentLimit: null, environmentsUsed: 0, secretVersioning: true, diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 9d2c5a472..d69f7bf95 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -15,6 +15,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ membersUsed: 0, environmentLimit: null, environmentsUsed: 0, + identityLimit: null, + identitiesUsed: 0, dynamicSecret: false, secretVersioning: true, pitRecovery: false, @@ -27,6 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ auditLogStreams: false, auditLogStreamLimit: 3, samlSSO: false, + oidcSSO: false, scim: false, ldap: false, groups: false, diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index cf7048801..cab428e86 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -9,7 +9,7 @@ export type TLicenseDALFactory = ReturnType; export const licenseDALFactory = (db: TDbClient) => { const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.OrgMembership) + const doc = await (tx || db.replicaNode())(TableName.OrgMembership) .where({ status: OrgMembershipStatus.Accepted }) .andWhere((bd) => { if (orgId) { @@ -19,11 +19,44 @@ export const licenseDALFactory = (db: TDbClient) => { .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) .count(); - return doc?.[0].count; + return Number(doc?.[0].count); } catch (error) { throw new DatabaseError({ error, name: "Count of Org Members" }); } }; - return { countOfOrgMembers }; + const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { + try { + // count org users + const userDoc = await (tx || db)(TableName.OrgMembership) + .where({ status: OrgMembershipStatus.Accepted }) + .andWhere((bd) => { + if (orgId) { + void bd.where({ orgId }); + } + }) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .where(`${TableName.Users}.isGhost`, false) + .count(); + + const userCount = Number(userDoc?.[0].count); + + // count org identities + const identityDoc = await (tx || db)(TableName.IdentityOrgMembership) + .where((bd) => { + if (orgId) { + void bd.where({ orgId }); + } + }) + .count(); + + const identityCount = Number(identityDoc?.[0].count); + + return userCount + identityCount; + } catch (error) { + throw new DatabaseError({ error, name: "Count of Org Users + Identities" }); + } + }; + + return { countOfOrgMembers, countOrgUsersAndIdentities }; }; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 46931468f..f0b568535 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -5,6 +5,7 @@ // TODO(akhilmhdh): With tony find out the api structure and fill it here import { ForbiddenError } from "@casl/ability"; +import { Knex } from "knex"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; @@ -155,6 +156,7 @@ export const licenseServiceFactory = ({ LICENSE_SERVER_CLOUD_PLAN_TTL, JSON.stringify(currentPlan) ); + return currentPlan; } } catch (error) { @@ -199,21 +201,27 @@ export const licenseServiceFactory = ({ await licenseServerCloudApi.request.delete(`/api/license-server/v1/customers/${customerId}`); }; - const updateSubscriptionOrgMemberCount = async (orgId: string) => { + const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { if (instanceType === InstanceType.Cloud) { const org = await orgDAL.findOrgById(orgId); if (!org) throw new BadRequestError({ message: "Org not found" }); - const count = await licenseDAL.countOfOrgMembers(orgId); + const quantity = await licenseDAL.countOfOrgMembers(orgId, tx); + const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx); if (org?.customerId) { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { - quantity: count + quantity, + quantityIdentities }); } 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 }); + const usedSeats = await licenseDAL.countOfOrgMembers(null, tx); + const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx); + await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { + usedSeats, + usedIdentitySeats + }); } await refreshPlan(orgId); }; diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index e23ff2c84..36b03ff80 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -31,6 +31,8 @@ export type TFeatureSet = { dynamicSecret: false; memberLimit: null; membersUsed: 0; + identityLimit: null; + identitiesUsed: 0; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; @@ -44,6 +46,7 @@ export type TFeatureSet = { auditLogStreams: false; auditLogStreamLimit: 3; samlSSO: false; + oidcSSO: false; scim: false; ldap: false; groups: false; diff --git a/backend/src/ee/services/oidc/oidc-config-dal.ts b/backend/src/ee/services/oidc/oidc-config-dal.ts new file mode 100644 index 000000000..470916c61 --- /dev/null +++ b/backend/src/ee/services/oidc/oidc-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 TOidcConfigDALFactory = ReturnType; + +export const oidcConfigDALFactory = (db: TDbClient) => { + const oidcCfgOrm = ormify(db, TableName.OidcConfig); + + return { ...oidcCfgOrm }; +}; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts new file mode 100644 index 000000000..55c929b9a --- /dev/null +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -0,0 +1,645 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; +import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; + +import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; +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 { 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 { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; +import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; +import { TokenType } from "@app/services/auth-token/auth-token-types"; +import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; +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 { UserAliasType } from "@app/services/user-alias/user-alias-types"; + +import { TOidcConfigDALFactory } from "./oidc-config-dal"; +import { + OIDCConfigurationType, + TCreateOidcCfgDTO, + TGetOidcCfgDTO, + TOidcLoginDTO, + TUpdateOidcCfgDTO +} from "./oidc-config-types"; + +type TOidcConfigServiceFactoryDep = { + userDAL: Pick< + TUserDALFactory, + "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId" + >; + userAliasDAL: Pick; + orgDAL: Pick< + TOrgDALFactory, + "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" + >; + orgMembershipDAL: Pick; + orgBotDAL: Pick; + licenseService: Pick; + tokenService: Pick; + smtpService: Pick; + permissionService: Pick; + oidcConfigDAL: Pick; +}; + +export type TOidcConfigServiceFactory = ReturnType; + +export const oidcConfigServiceFactory = ({ + orgDAL, + orgMembershipDAL, + userDAL, + userAliasDAL, + licenseService, + permissionService, + tokenService, + orgBotDAL, + smtpService, + oidcConfigDAL +}: TOidcConfigServiceFactoryDep) => { + const getOidc = async (dto: TGetOidcCfgDTO) => { + const org = await orgDAL.findOne({ slug: dto.orgSlug }); + if (!org) { + throw new BadRequestError({ + message: "Organization not found", + name: "OrgNotFound" + }); + } + if (dto.type === "external") { + const { permission } = await permissionService.getOrgPermission( + dto.actor, + dto.actorId, + org.id, + dto.actorAuthMethod, + dto.actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); + } + + const oidcCfg = await oidcConfigDAL.findOne({ + orgId: org.id + }); + + if (!oidcCfg) { + throw new BadRequestError({ + message: "Failed to find organization OIDC configuration" + }); + } + + // decrypt and return cfg + const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.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 { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } = + oidcCfg; + + let clientId = ""; + if (encryptedClientId && clientIdIV && clientIdTag) { + clientId = decryptSymmetric({ + ciphertext: encryptedClientId, + key, + tag: clientIdTag, + iv: clientIdIV + }); + } + + let clientSecret = ""; + if (encryptedClientSecret && clientSecretIV && clientSecretTag) { + clientSecret = decryptSymmetric({ + key, + tag: clientSecretTag, + iv: clientSecretIV, + ciphertext: encryptedClientSecret + }); + } + + return { + id: oidcCfg.id, + issuer: oidcCfg.issuer, + authorizationEndpoint: oidcCfg.authorizationEndpoint, + configurationType: oidcCfg.configurationType, + discoveryURL: oidcCfg.discoveryURL, + jwksUri: oidcCfg.jwksUri, + tokenEndpoint: oidcCfg.tokenEndpoint, + userinfoEndpoint: oidcCfg.userinfoEndpoint, + orgId: oidcCfg.orgId, + isActive: oidcCfg.isActive, + allowedEmailDomains: oidcCfg.allowedEmailDomains, + clientId, + clientSecret + }; + }; + + const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => { + const serverCfg = await getServerCfg(); + + if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) { + throw new BadRequestError({ + message: "Login with OIDC is disabled by administrator." + }); + } + + const appCfg = getConfig(); + const userAlias = await userAliasDAL.findOne({ + externalId, + orgId, + aliasType: UserAliasType.OIDC + }); + + const organization = await orgDAL.findOrgById(orgId); + if (!organization) throw new BadRequestError({ message: "Org not found" }); + + let user: TUsers; + if (userAlias) { + user = await userDAL.transaction(async (tx) => { + const foundUser = await userDAL.findById(userAlias.userId, tx); + const [orgMembership] = await orgDAL.findMembership( + { + [`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id, + [`${TableName.OrgMembership}.orgId` as "id"]: orgId + }, + { tx } + ); + if (!orgMembership) { + await orgMembershipDAL.create( + { + userId: userAlias.userId, + inviteEmail: email, + orgId, + role: OrgMembershipRole.Member, + status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + }, + tx + ); + // Only update the membership to Accepted if the user account is already completed. + } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) { + await orgDAL.updateMembershipById( + orgMembership.id, + { + status: OrgMembershipStatus.Accepted + }, + tx + ); + } + + return foundUser; + }); + } else { + user = await userDAL.transaction(async (tx) => { + let newUser: TUsers | undefined; + + if (serverCfg.trustOidcEmails) { + newUser = await userDAL.findOne( + { + email, + isEmailVerified: true + }, + tx + ); + } + + if (!newUser) { + const uniqueUsername = await normalizeUsername(externalId, userDAL); + newUser = await userDAL.create( + { + email, + firstName, + isEmailVerified: serverCfg.trustOidcEmails, + username: serverCfg.trustOidcEmails ? email : uniqueUsername, + lastName, + authMethods: [], + isGhost: false + }, + tx + ); + } + + await userAliasDAL.create( + { + userId: newUser.id, + aliasType: UserAliasType.OIDC, + externalId, + emails: email ? [email] : [], + orgId + }, + tx + ); + + const [orgMembership] = await orgDAL.findMembership( + { + [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, + [`${TableName.OrgMembership}.orgId` as "id"]: orgId + }, + { tx } + ); + + if (!orgMembership) { + await orgMembershipDAL.create( + { + userId: newUser.id, + inviteEmail: email, + orgId, + role: OrgMembershipRole.Member, + status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + }, + tx + ); + // Only update the membership to Accepted if the user account is already completed. + } else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) { + await orgDAL.updateMembershipById( + orgMembership.id, + { + status: OrgMembershipStatus.Accepted + }, + tx + ); + } + + return newUser; + }); + } + + await licenseService.updateSubscriptionOrgMemberCount(organization.id); + + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); + const isUserCompleted = Boolean(user.isAccepted); + const providerAuthToken = jwt.sign( + { + authTokenType: AuthTokenType.PROVIDER_TOKEN, + userId: user.id, + username: user.username, + ...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }), + firstName, + lastName, + organizationName: organization.name, + organizationId: organization.id, + organizationSlug: organization.slug, + hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey), + authMethod: AuthMethod.OIDC, + authType: UserAliasType.OIDC, + isUserCompleted, + ...(callbackPort && { callbackPort }) + }, + appCfg.AUTH_SECRET, + { + expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME + } + ); + + if (user.email && !user.isEmailVerified) { + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_VERIFICATION, + userId: user.id + }); + + await smtpService.sendMail({ + template: SmtpTemplates.EmailVerification, + subjectLine: "Infisical confirmation code", + recipients: [user.email], + substitutions: { + code: token + } + }); + } + + return { isUserCompleted, providerAuthToken }; + }; + + const updateOidcCfg = async ({ + orgSlug, + allowedEmailDomains, + configurationType, + discoveryURL, + actor, + actorOrgId, + actorAuthMethod, + actorId, + issuer, + isActive, + authorizationEndpoint, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + clientId, + clientSecret + }: TUpdateOidcCfgDTO) => { + const org = await orgDAL.findOne({ + slug: orgSlug + }); + + if (!org) { + throw new BadRequestError({ + message: "Organization not found" + }); + } + + const plan = await licenseService.getPlan(org.id); + if (!plan.oidcSSO) + throw new BadRequestError({ + message: + "Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." + }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + org.id, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); + + const orgBot = await orgBotDAL.findOne({ orgId: org.id }); + 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 updateQuery: TOidcConfigsUpdate = { + allowedEmailDomains, + configurationType, + discoveryURL, + issuer, + authorizationEndpoint, + tokenEndpoint, + userinfoEndpoint, + jwksUri, + isActive + }; + + if (clientId !== undefined) { + const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); + updateQuery.encryptedClientId = encryptedClientId; + updateQuery.clientIdIV = clientIdIV; + updateQuery.clientIdTag = clientIdTag; + } + + if (clientSecret !== undefined) { + const { + ciphertext: encryptedClientSecret, + iv: clientSecretIV, + tag: clientSecretTag + } = encryptSymmetric(clientSecret, key); + + updateQuery.encryptedClientSecret = encryptedClientSecret; + updateQuery.clientSecretIV = clientSecretIV; + updateQuery.clientSecretTag = clientSecretTag; + } + + const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); + return ssoConfig; + }; + + const createOidcCfg = async ({ + orgSlug, + allowedEmailDomains, + configurationType, + discoveryURL, + actor, + actorOrgId, + actorAuthMethod, + actorId, + issuer, + isActive, + authorizationEndpoint, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + clientId, + clientSecret + }: TCreateOidcCfgDTO) => { + const org = await orgDAL.findOne({ + slug: orgSlug + }); + if (!org) { + throw new BadRequestError({ + message: "Organization not found" + }); + } + + const plan = await licenseService.getPlan(org.id); + if (!plan.oidcSSO) + throw new BadRequestError({ + message: + "Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." + }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + org.id, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); + + const orgBot = await orgBotDAL.transaction(async (tx) => { + const doc = await orgBotDAL.findOne({ orgId: org.id }, 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: org.id, + privateKeyTag, + privateKeyAlgorithm, + privateKeyKeyEncoding, + symmetricKeyKeyEncoding + }, + tx + ); + }); + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); + const { + ciphertext: encryptedClientSecret, + iv: clientSecretIV, + tag: clientSecretTag + } = encryptSymmetric(clientSecret, key); + + const oidcCfg = await oidcConfigDAL.create({ + issuer, + isActive, + configurationType, + discoveryURL, + authorizationEndpoint, + allowedEmailDomains, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + orgId: org.id, + encryptedClientId, + clientIdIV, + clientIdTag, + encryptedClientSecret, + clientSecretIV, + clientSecretTag + }); + + return oidcCfg; + }; + + const getOrgAuthStrategy = async (orgSlug: string, callbackPort?: string) => { + const appCfg = getConfig(); + + const org = await orgDAL.findOne({ + slug: orgSlug + }); + + if (!org) { + throw new BadRequestError({ + message: "Organization not found." + }); + } + + const oidcCfg = await getOidc({ + type: "internal", + orgSlug + }); + + if (!oidcCfg || !oidcCfg.isActive) { + throw new BadRequestError({ + message: "Failed to authenticate with OIDC SSO" + }); + } + + let issuer: Issuer; + if (oidcCfg.configurationType === OIDCConfigurationType.DISCOVERY_URL) { + if (!oidcCfg.discoveryURL) { + throw new BadRequestError({ + message: "OIDC not configured correctly" + }); + } + issuer = await Issuer.discover(oidcCfg.discoveryURL); + } else { + if ( + !oidcCfg.issuer || + !oidcCfg.authorizationEndpoint || + !oidcCfg.jwksUri || + !oidcCfg.tokenEndpoint || + !oidcCfg.userinfoEndpoint + ) { + throw new BadRequestError({ + message: "OIDC not configured correctly" + }); + } + issuer = new OpenIdIssuer({ + issuer: oidcCfg.issuer, + authorization_endpoint: oidcCfg.authorizationEndpoint, + jwks_uri: oidcCfg.jwksUri, + token_endpoint: oidcCfg.tokenEndpoint, + userinfo_endpoint: oidcCfg.userinfoEndpoint + }); + } + + const client = new issuer.Client({ + client_id: oidcCfg.clientId, + client_secret: oidcCfg.clientSecret, + redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`] + }); + + const strategy = new OpenIdStrategy( + { + client, + passReqToCallback: true + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_req: any, tokenSet: TokenSet, cb: any) => { + const claims = tokenSet.claims(); + if (!claims.email || !claims.given_name) { + throw new BadRequestError({ + message: "Invalid request. Missing email or first name" + }); + } + + if (oidcCfg.allowedEmailDomains) { + const allowedDomains = oidcCfg.allowedEmailDomains.split(", "); + if (!allowedDomains.includes(claims.email.split("@")[1])) { + throw new BadRequestError({ + message: "Email not allowed." + }); + } + } + + oidcLogin({ + email: claims.email, + externalId: claims.sub, + firstName: claims.given_name ?? "", + lastName: claims.family_name ?? "", + orgId: org.id, + callbackPort + }) + .then(({ isUserCompleted, providerAuthToken }) => { + cb(null, { isUserCompleted, providerAuthToken }); + }) + .catch((error) => { + cb(error); + }); + } + ); + + return strategy; + }; + + return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg }; +}; diff --git a/backend/src/ee/services/oidc/oidc-config-types.ts b/backend/src/ee/services/oidc/oidc-config-types.ts new file mode 100644 index 000000000..6e36b796b --- /dev/null +++ b/backend/src/ee/services/oidc/oidc-config-types.ts @@ -0,0 +1,56 @@ +import { TGenericPermission } from "@app/lib/types"; + +export enum OIDCConfigurationType { + CUSTOM = "custom", + DISCOVERY_URL = "discoveryURL" +} + +export type TOidcLoginDTO = { + externalId: string; + email: string; + firstName: string; + lastName?: string; + orgId: string; + callbackPort?: string; +}; + +export type TGetOidcCfgDTO = + | ({ + type: "external"; + orgSlug: string; + } & TGenericPermission) + | { + type: "internal"; + orgSlug: string; + }; + +export type TCreateOidcCfgDTO = { + issuer?: string; + authorizationEndpoint?: string; + discoveryURL?: string; + configurationType: OIDCConfigurationType; + allowedEmailDomains?: string; + jwksUri?: string; + tokenEndpoint?: string; + userinfoEndpoint?: string; + clientId: string; + clientSecret: string; + isActive: boolean; + orgSlug: string; +} & TGenericPermission; + +export type TUpdateOidcCfgDTO = Partial<{ + issuer: string; + authorizationEndpoint: string; + allowedEmailDomains: string; + discoveryURL: string; + jwksUri: string; + configurationType: OIDCConfigurationType; + tokenEndpoint: string; + userinfoEndpoint: string; + clientId: string; + clientSecret: string; + isActive: boolean; + orgSlug: string; +}> & + TGenericPermission; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 9fece040b..6b7b3b2b2 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -116,7 +116,6 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index d8114388e..d228ae109 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -10,7 +10,8 @@ export type TPermissionDALFactory = ReturnType; export const permissionDALFactory = (db: TDbClient) => { const getOrgPermission = async (userId: string, orgId: string) => { try { - const membership = await db(TableName.OrgMembership) + const membership = await db + .replicaNode()(TableName.OrgMembership) .leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) .where("userId", userId) @@ -28,7 +29,8 @@ export const permissionDALFactory = (db: TDbClient) => { const getOrgIdentityPermission = async (identityId: string, orgId: string) => { try { - const membership = await db(TableName.IdentityOrgMembership) + const membership = await db + .replicaNode()(TableName.IdentityOrgMembership) .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`) .where("identityId", identityId) @@ -45,11 +47,13 @@ export const permissionDALFactory = (db: TDbClient) => { const getProjectPermission = async (userId: string, projectId: string) => { try { - const groups: string[] = await db(TableName.GroupProjectMembership) + const groups: string[] = await db + .replicaNode()(TableName.GroupProjectMembership) .where(`${TableName.GroupProjectMembership}.projectId`, projectId) .pluck(`${TableName.GroupProjectMembership}.groupId`); - const groupDocs = await db(TableName.UserGroupMembership) + const groupDocs = await db + .replicaNode()(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.userId`, userId) .whereIn(`${TableName.UserGroupMembership}.groupId`, groups) .join( @@ -231,7 +235,8 @@ export const permissionDALFactory = (db: TDbClient) => { const getProjectIdentityPermission = async (identityId: string, projectId: string) => { try { - const docs = await db(TableName.IdentityProjectMembership) + const docs = await db + .replicaNode()(TableName.IdentityProjectMembership) .join( TableName.IdentityProjectMembershipRole, `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, diff --git a/backend/src/ee/services/saml-config/saml-config-dal.ts b/backend/src/ee/services/saml-config/saml-config-dal.ts index 1e7b9e47e..aff42230f 100644 --- a/backend/src/ee/services/saml-config/saml-config-dal.ts +++ b/backend/src/ee/services/saml-config/saml-config-dal.ts @@ -10,7 +10,8 @@ export const samlConfigDALFactory = (db: TDbClient) => { const findEnforceableSamlCfg = async (orgId: string) => { try { - const samlCfg = await db(TableName.SamlConfig) + const samlCfg = await db + .replicaNode()(TableName.SamlConfig) .where({ orgId, isActive: true 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 3cc51e1c2..c147b7e27 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -28,6 +28,7 @@ import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; 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"; @@ -335,6 +336,13 @@ export const samlConfigServiceFactory = ({ }: TSamlLoginDTO) => { const appCfg = getConfig(); const serverCfg = await getServerCfg(); + + if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.SAML)) { + throw new BadRequestError({ + message: "Login with SAML is disabled by administrator." + }); + } + const userAlias = await userAliasDAL.findOne({ externalId, orgId, @@ -380,6 +388,21 @@ export const samlConfigServiceFactory = ({ return foundUser; }); } else { + const plan = await licenseService.getPlan(orgId); + if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + // limit imposed on number of members allowed / number of members used exceeds the number of members allowed + throw new BadRequestError({ + message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." + }); + } + + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." + }); + } + user = await userDAL.transaction(async (tx) => { let newUser: TUsers | undefined; if (serverCfg.trustSamlEmails) { diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index eec3d9a1d..daf6d0bd8 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -1,49 +1,59 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TSecretApprovalPolicies } from "@app/db/schemas"; +import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex"; +import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; export type TSecretApprovalPolicyDALFactory = ReturnType; export const secretApprovalPolicyDALFactory = (db: TDbClient) => { const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy); - const sapFindQuery = (tx: Knex, filter: TFindFilter) => + const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter) => tx(TableName.SecretApprovalPolicy) // eslint-disable-next-line .where(buildFindFilter(filter)) .join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`) - .join( + .leftJoin( TableName.SecretApprovalPolicyApprover, `${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicyApprover}.policyId` ) - .select(tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)) - .select(tx.ref("name").withSchema(TableName.Environment).as("envName")) - .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) - .select(tx.ref("id").withSchema(TableName.Environment).as("envId")) - .select(tx.ref("projectId").withSchema(TableName.Environment)) + .select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover)) + .select( + tx.ref("name").withSchema(TableName.Environment).as("envName"), + tx.ref("slug").withSchema(TableName.Environment).as("envSlug"), + tx.ref("id").withSchema(TableName.Environment).as("envId"), + tx.ref("projectId").withSchema(TableName.Environment) + ) .select(selectAllTableCols(TableName.SecretApprovalPolicy)) .orderBy("createdAt", "asc"); const findById = async (id: string, tx?: Knex) => { try { - const doc = await sapFindQuery(tx || db, { + const doc = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), { [`${TableName.SecretApprovalPolicy}.id` as "id"]: id }); - const formatedDoc = mergeOneToManyRelation( - doc, - "id", - ({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({ - ...el, - envId, - environment: { id: envId, name, slug } + const formatedDoc = sqlNestRelationships({ + data: doc, + key: "id", + parentMapper: (data) => ({ + environment: { id: data.envId, name: data.envName, slug: data.envSlug }, + projectId: data.projectId, + ...SecretApprovalPoliciesSchema.parse(data) }), - ({ approverId }) => approverId, - "approvers" - ); + childrenMapper: [ + { + key: "approverUserId", + label: "userApprovers" as const, + mapper: ({ approverUserId }) => ({ + userId: approverUserId + }) + } + ] + }); + return formatedDoc?.[0]; } catch (error) { throw new DatabaseError({ error, name: "FindById" }); @@ -52,18 +62,25 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { const find = async (filter: TFindFilter, tx?: Knex) => { try { - const docs = await sapFindQuery(tx || db, filter); - const formatedDoc = mergeOneToManyRelation( - docs, - "id", - ({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({ - ...el, - envId, - environment: { id: envId, name, slug } + const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter); + const formatedDoc = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (data) => ({ + environment: { id: data.envId, name: data.envName, slug: data.envSlug }, + projectId: data.projectId, + ...SecretApprovalPoliciesSchema.parse(data) }), - ({ approverId }) => approverId, - "approvers" - ); + childrenMapper: [ + { + key: "approverUserId", + label: "userApprovers" as const, + mapper: ({ approverUserId }) => ({ + userId: approverUserId + }) + } + ] + }); return formatedDoc; } catch (error) { throw new DatabaseError({ error, name: "Find" }); diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index f99384de6..2db825c88 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -7,7 +7,6 @@ import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; import { containsGlobPatterns } from "@app/lib/picomatch"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; @@ -29,7 +28,6 @@ type TSecretApprovalPolicyServiceFactoryDep = { secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; projectEnvDAL: Pick; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; - projectMembershipDAL: Pick; }; export type TSecretApprovalPolicyServiceFactory = ReturnType; @@ -38,8 +36,7 @@ export const secretApprovalPolicyServiceFactory = ({ secretApprovalPolicyDAL, permissionService, secretApprovalPolicyApproverDAL, - projectEnvDAL, - projectMembershipDAL + projectEnvDAL }: TSecretApprovalPolicyServiceFactoryDep) => { const createSecretApprovalPolicy = async ({ name, @@ -48,12 +45,12 @@ export const secretApprovalPolicyServiceFactory = ({ actorOrgId, actorAuthMethod, approvals, - approvers, + approverUserIds, projectId, secretPath, environment }: TCreateSapDTO) => { - if (approvals > approvers.length) + if (approvals > approverUserIds.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); const { permission } = await permissionService.getProjectPermission( @@ -70,13 +67,6 @@ export const secretApprovalPolicyServiceFactory = ({ const env = await projectEnvDAL.findOne({ slug: environment, projectId }); if (!env) throw new BadRequestError({ message: "Environment not found" }); - const secretApprovers = await projectMembershipDAL.find({ - projectId, - $in: { id: approvers } - }); - if (secretApprovers.length !== approvers.length) - throw new BadRequestError({ message: "Approver not found in project" }); - const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => { const doc = await secretApprovalPolicyDAL.create( { @@ -88,8 +78,8 @@ export const secretApprovalPolicyServiceFactory = ({ tx ); await secretApprovalPolicyApproverDAL.insertMany( - secretApprovers.map(({ id }) => ({ - approverId: id, + approverUserIds.map((approverUserId) => ({ + approverUserId, policyId: doc.id })), tx @@ -100,7 +90,7 @@ export const secretApprovalPolicyServiceFactory = ({ }; const updateSecretApprovalPolicy = async ({ - approvers, + approverUserIds, secretPath, name, actorId, @@ -132,22 +122,11 @@ export const secretApprovalPolicyServiceFactory = ({ }, tx ); - if (approvers) { - const secretApprovers = await projectMembershipDAL.find( - { - projectId: secretApprovalPolicy.projectId, - $in: { id: approvers } - }, - { tx } - ); - if (secretApprovers.length !== approvers.length) - throw new BadRequestError({ message: "Approver not found in project" }); - if (doc.approvals > secretApprovers.length) - throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + if (approverUserIds) { await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); await secretApprovalPolicyApproverDAL.insertMany( - secretApprovers.map(({ id }) => ({ - approverId: id, + approverUserIds.map((approverUserId) => ({ + approverUserId, policyId: doc.id })), tx diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index 2ddd9b51b..1a527289c 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -4,7 +4,7 @@ export type TCreateSapDTO = { approvals: number; secretPath?: string | null; environment: string; - approvers: string[]; + approverUserIds: string[]; projectId: string; name: string; } & Omit; @@ -13,7 +13,7 @@ export type TUpdateSapDTO = { secretPolicyId: string; approvals?: number; secretPath?: string | null; - approvers: string[]; + approverUserIds: string[]; name?: string; } & Omit; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 05fe1b8f8..06c48ac8b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -5,7 +5,8 @@ import { SecretApprovalRequestsSchema, TableName, TSecretApprovalRequests, - TSecretApprovalRequestsSecrets + TSecretApprovalRequestsSecrets, + TUsers } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships, stripUndefinedInWhere, TFindFilter } from "@app/lib/knex"; @@ -16,7 +17,7 @@ export type TSecretApprovalRequestDALFactory = ReturnType { `${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalPolicy}.id` ) + .leftJoin( + db(TableName.Users).as("statusChangedByUser"), + `${TableName.SecretApprovalRequest}.statusChangedByUserId`, + `statusChangedByUser.id` + ) + .join( + db(TableName.Users).as("committerUser"), + `${TableName.SecretApprovalRequest}.committerUserId`, + `committerUser.id` + ) .join( TableName.SecretApprovalPolicyApprover, `${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicyApprover}.policyId` ) + .join( + db(TableName.Users).as("secretApprovalPolicyApproverUser"), + `${TableName.SecretApprovalPolicyApprover}.approverUserId`, + "secretApprovalPolicyApproverUser.id" + ) .leftJoin( TableName.SecretApprovalRequestReviewer, `${TableName.SecretApprovalRequest}.id`, `${TableName.SecretApprovalRequestReviewer}.requestId` ) + .leftJoin( + db(TableName.Users).as("secretApprovalReviewerUser"), + `${TableName.SecretApprovalRequestReviewer}.reviewerUserId`, + `secretApprovalReviewerUser.id` + ) .select(selectAllTableCols(TableName.SecretApprovalRequest)) .select( - tx.ref("member").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"), + tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), + tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"), + tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"), + tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"), + tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"), + tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"), + tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"), + tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"), + tx.ref("lastName").withSchema("statusChangedByUser").as("statusChangedByUserLastName"), + tx.ref("email").withSchema("committerUser").as("committerUserEmail"), + tx.ref("username").withSchema("committerUser").as("committerUserUsername"), + tx.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), + tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"), + tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer), tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"), + tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"), + tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"), + tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"), + tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"), tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"), tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"), tx.ref("projectId").withSchema(TableName.Environment), tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), - tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), - tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover) + tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") ); const findById = async (id: string, tx?: Knex) => { try { - const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db); + const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db.replicaNode()); const docs = await sql; const formatedDoc = sqlNestRelationships({ data: docs, @@ -71,6 +108,22 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { ...SecretApprovalRequestsSchema.parse(el), projectId: el.projectId, environment: el.environment, + statusChangedByUser: el.statusChangedByUserId + ? { + userId: el.statusChangedByUserId, + email: el.statusChangedByUserEmail, + firstName: el.statusChangedByUserFirstName, + lastName: el.statusChangedByUserLastName, + username: el.statusChangedByUserUsername + } + : undefined, + committerUser: { + userId: el.committerUserId, + email: el.committerUserEmail, + firstName: el.committerUserFirstName, + lastName: el.committerUserLastName, + username: el.committerUserUsername + }, policy: { id: el.policyId, name: el.policyName, @@ -80,11 +133,34 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { }), childrenMapper: [ { - key: "reviewerMemberId", + key: "reviewerUserId", label: "reviewers" as const, - mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + mapper: ({ + reviewerUserId: userId, + reviewerStatus: status, + reviewerEmail: email, + reviewerLastName: lastName, + reviewerUsername: username, + reviewerFirstName: firstName + }) => (userId ? { userId, status, email, firstName, lastName, username } : undefined) }, - { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + { + key: "approverUserId", + label: "approvers" as const, + mapper: ({ + approverUserId, + approverEmail: email, + approverUsername: username, + approverLastName: lastName, + approverFirstName: firstName + }) => ({ + userId: approverUserId, + email, + firstName, + lastName, + username + }) + } ] }); if (!formatedDoc?.[0]) return; @@ -97,12 +173,12 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { } }; - const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => { + const findProjectRequestCount = async (projectId: string, userId: string, tx?: Knex) => { try { const docs = await (tx || db) .with( "temp", - (tx || db)(TableName.SecretApprovalRequest) + (tx || db.replicaNode())(TableName.SecretApprovalRequest) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join( @@ -114,8 +190,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .andWhere( (bd) => void bd - .where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId) - .orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId) + .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) + .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) ) .select("status", `${TableName.SecretApprovalRequest}.id`) .groupBy(`${TableName.SecretApprovalRequest}.id`, "status") @@ -142,13 +218,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { }; const findByProjectId = async ( - { status, limit = 20, offset = 0, projectId, committer, environment, membershipId }: TFindQueryFilter, + { status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter, tx?: Knex ) => { try { // akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination // this is the place u wanna look at. - const query = (tx || db)(TableName.SecretApprovalRequest) + const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join( @@ -161,6 +237,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalPolicy}.id`, `${TableName.SecretApprovalPolicyApprover}.policyId` ) + .join( + db(TableName.Users).as("committerUser"), + `${TableName.SecretApprovalRequest}.committerUserId`, + `committerUser.id` + ) .leftJoin( TableName.SecretApprovalRequestReviewer, `${TableName.SecretApprovalRequest}.id`, @@ -176,20 +257,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { projectId, [`${TableName.Environment}.slug` as "slug"]: environment, [`${TableName.SecretApprovalRequest}.status`]: status, - committerId: committer + committerUserId: committer }) ) .andWhere( (bd) => void bd - .where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId) - .orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId) + .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) + .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) ) .select(selectAllTableCols(TableName.SecretApprovalRequest)) .select( db.ref("projectId").withSchema(TableName.Environment), db.ref("slug").withSchema(TableName.Environment).as("environment"), - db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"), + db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"), + db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer), db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"), db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"), db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"), @@ -201,7 +283,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), - db.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover) + db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), + db.ref("email").withSchema("committerUser").as("committerUserEmail"), + db.ref("username").withSchema("committerUser").as("committerUserUsername"), + db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), + db.ref("lastName").withSchema("committerUser").as("committerUserLastName") ) .orderBy("createdAt", "desc"); @@ -223,18 +309,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { name: el.policyName, approvals: el.policyApprovals, secretPath: el.policySecretPath + }, + committerUser: { + userId: el.committerUserId, + email: el.committerUserEmail, + firstName: el.committerUserFirstName, + lastName: el.committerUserLastName, + username: el.committerUserUsername } }), childrenMapper: [ { - key: "reviewerMemberId", + key: "reviewerId", label: "reviewers" as const, - mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => (member ? { member, status: s } : undefined) + mapper: ({ reviewerUserId, reviewerStatus: s }) => + reviewerUserId ? { userId: reviewerUserId, status: s } : undefined }, { - key: "approverId", + key: "approverUserId", label: "approvers" as const, - mapper: ({ approverId }) => approverId + mapper: ({ approverUserId }) => approverUserId }, { key: "commitId", 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 736cd253e..8dc06aaf5 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 @@ -47,7 +47,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { const findByRequestId = async (requestId: string, tx?: Knex) => { try { - const doc = await (tx || db)({ + const doc = await (tx || db.replicaNode())({ secVerTag: TableName.SecretTag }) .from(TableName.SecretApprovalRequestSecret) 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 5d0977134..a519af4fd 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 @@ -87,7 +87,7 @@ export const secretApprovalRequestServiceFactory = ({ const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - const { membership } = await permissionService.getProjectPermission( + await permissionService.getProjectPermission( actor as ActorType.USER, actorId, projectId, @@ -95,7 +95,7 @@ export const secretApprovalRequestServiceFactory = ({ actorOrgId ); - const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id); + const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId); return count; }; @@ -113,19 +113,13 @@ export const secretApprovalRequestServiceFactory = ({ }: TListApprovalsDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - const { membership } = await permissionService.getProjectPermission( - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId - ); + await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId); const approvals = await secretApprovalRequestDAL.findByProjectId({ projectId, committer, environment, status, - membershipId: membership.id, + userId: actorId, limit, offset }); @@ -145,7 +139,7 @@ export const secretApprovalRequestServiceFactory = ({ if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); const { policy } = secretApprovalRequest; - const { membership, hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission( actor, actorId, secretApprovalRequest.projectId, @@ -154,8 +148,8 @@ export const secretApprovalRequestServiceFactory = ({ ); if ( !hasRole(ProjectMembershipRole.Admin) && - secretApprovalRequest.committerId !== membership.id && - !policy.approvers.find((approverId) => approverId === membership.id) + secretApprovalRequest.committerUserId !== actorId && + !policy.approvers.find(({ userId }) => userId === actorId) ) { throw new UnauthorizedError({ message: "User has no access" }); } @@ -180,7 +174,7 @@ export const secretApprovalRequestServiceFactory = ({ if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); const { policy } = secretApprovalRequest; - const { membership, hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, actorId, secretApprovalRequest.projectId, @@ -189,8 +183,8 @@ export const secretApprovalRequestServiceFactory = ({ ); if ( !hasRole(ProjectMembershipRole.Admin) && - secretApprovalRequest.committerId !== membership.id && - !policy.approvers.find((approverId) => approverId === membership.id) + secretApprovalRequest.committerUserId !== actorId && + !policy.approvers.find(({ userId }) => userId === actorId) ) { throw new UnauthorizedError({ message: "User has no access" }); } @@ -198,7 +192,7 @@ export const secretApprovalRequestServiceFactory = ({ const review = await secretApprovalRequestReviewerDAL.findOne( { requestId: secretApprovalRequest.id, - member: membership.id + reviewerUserId: actorId }, tx ); @@ -207,7 +201,7 @@ export const secretApprovalRequestServiceFactory = ({ { status, requestId: secretApprovalRequest.id, - member: membership.id + reviewerUserId: actorId }, tx ); @@ -230,7 +224,7 @@ export const secretApprovalRequestServiceFactory = ({ if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); const { policy } = secretApprovalRequest; - const { membership, hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, actorId, secretApprovalRequest.projectId, @@ -239,8 +233,8 @@ export const secretApprovalRequestServiceFactory = ({ ); if ( !hasRole(ProjectMembershipRole.Admin) && - secretApprovalRequest.committerId !== membership.id && - !policy.approvers.find((approverId) => approverId === membership.id) + secretApprovalRequest.committerUserId !== actorId && + !policy.approvers.find(({ userId }) => userId === actorId) ) { throw new UnauthorizedError({ message: "User has no access" }); } @@ -253,7 +247,7 @@ export const secretApprovalRequestServiceFactory = ({ const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, { status, - statusChangeBy: membership.id + statusChangedByUserId: actorId }); return { ...secretApprovalRequest, ...updatedRequest }; }; @@ -270,7 +264,7 @@ export const secretApprovalRequestServiceFactory = ({ if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" }); const { policy, folderId, projectId } = secretApprovalRequest; - const { membership, hasRole } = await permissionService.getProjectPermission( + const { hasRole } = await permissionService.getProjectPermission( ActorType.USER, actorId, projectId, @@ -280,19 +274,19 @@ export const secretApprovalRequestServiceFactory = ({ if ( !hasRole(ProjectMembershipRole.Admin) && - secretApprovalRequest.committerId !== membership.id && - !policy.approvers.find((approverId) => approverId === membership.id) + secretApprovalRequest.committerUserId !== actorId && + !policy.approvers.find(({ userId }) => userId === actorId) ) { throw new UnauthorizedError({ message: "User has no access" }); } const reviewers = secretApprovalRequest.reviewers.reduce>( - (prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status as ApprovalStatus }), + (prev, curr) => ({ ...prev, [curr.userId.toString()]: curr.status as ApprovalStatus }), {} ); const hasMinApproval = secretApprovalRequest.policy.approvals <= secretApprovalRequest.policy.approvers.filter( - (approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED + ({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED ).length; if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); @@ -472,7 +466,7 @@ export const secretApprovalRequestServiceFactory = ({ conflicts: JSON.stringify(conflicts), hasMerged: true, status: RequestState.Closed, - statusChangeBy: membership.id + statusChangedByUserId: actorId }, tx ); @@ -509,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({ }: TGenerateSecretApprovalRequestDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - const { permission, membership } = await permissionService.getProjectPermission( + const { permission } = await permissionService.getProjectPermission( actor, actorId, projectId, @@ -663,7 +657,7 @@ export const secretApprovalRequestServiceFactory = ({ policyId: policy.id, status: "open", hasMerged: false, - committerId: membership.id + committerUserId: actorId }, tx ); diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index fd2f7cc1a..01f7d066c 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -11,7 +11,6 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { QueueName, TQueueServiceFactory } from "@app/queue"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns"; import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue"; @@ -46,7 +45,6 @@ type TSecretReplicationServiceFactoryDep = { secretBlindIndexDAL: Pick; secretTagDAL: Pick; secretApprovalRequestDAL: Pick; - projectMembershipDAL: Pick; secretApprovalRequestSecretDAL: Pick< TSecretApprovalRequestSecretDALFactory, "insertMany" | "insertApprovalSecretTags" @@ -92,7 +90,6 @@ export const secretReplicationServiceFactory = ({ secretApprovalRequestSecretDAL, secretApprovalRequestDAL, secretQueueService, - projectMembershipDAL, projectBotService }: TSecretReplicationServiceFactoryDep) => { const getReplicatedSecrets = ( @@ -297,12 +294,6 @@ export const secretReplicationServiceFactory = ({ ); // this means it should be a approval request rather than direct replication if (policy && actor === ActorType.USER) { - const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId }); - if (!membership) { - logger.error("Project membership not found in %s for user %s", projectId, actorId); - return; - } - const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id); const latestSecretVersions = await secretVersionDAL.findLatestVersionMany( destinationReplicationFolderId, @@ -316,7 +307,7 @@ export const secretReplicationServiceFactory = ({ policyId: policy.id, status: "open", hasMerged: false, - committerId: membership.id, + committerUserId: actorId, isReplicated: true }, tx diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-dal.ts b/backend/src/ee/services/secret-rotation/secret-rotation-dal.ts index 7feafdc6b..57d86ff04 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-dal.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-dal.ts @@ -41,7 +41,7 @@ export const secretRotationDALFactory = (db: TDbClient) => { const find = async (filter: TFindFilter, tx?: Knex) => { try { - const data = await findQuery(filter, tx || db); + const data = await findQuery(filter, tx || db.replicaNode()); return sqlNestRelationships({ data, key: "id", @@ -93,7 +93,7 @@ export const secretRotationDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.SecretRotation) + const doc = await (tx || db.replicaNode())(TableName.SecretRotation) .join(TableName.Environment, `${TableName.SecretRotation}.envId`, `${TableName.Environment}.id`) .where({ [`${TableName.SecretRotation}.id` as "id"]: id }) .select(selectAllTableCols(TableName.SecretRotation)) 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 140a9b671..e9eedaee8 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 @@ -331,7 +331,7 @@ export const secretRotationQueueFactory = ({ logger.info("Finished rotating: rotation id: ", rotationId); } catch (error) { - logger.error(error); + logger.error(error, "Failed to execute secret rotation"); if (error instanceof DisableRotationErrors) { if (job.id) { await queue.stopRepeatableJobByJobId(QueueName.SecretRotation, job.id); diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 1e1648a66..9b0109a35 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -133,7 +133,7 @@ export const secretRotationServiceFactory = ({ creds: [] }; const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData)); - const secretRotation = secretRotationDAL.transaction(async (tx) => { + const secretRotation = await secretRotationDAL.transaction(async (tx) => { const doc = await secretRotationDAL.create( { provider, @@ -148,13 +148,13 @@ export const secretRotationServiceFactory = ({ }, tx ); - await secretRotationQueue.addToQueue(doc.id, doc.interval); const outputSecretMapping = await secretRotationDAL.secretOutputInsertMany( Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })), tx ); return { ...doc, outputs: outputSecretMapping, environment: folder.environment }; }); + await secretRotationQueue.addToQueue(secretRotation.id, secretRotation.interval); return secretRotation; }; @@ -212,9 +212,9 @@ export const secretRotationServiceFactory = ({ ); const deletedDoc = await secretRotationDAL.transaction(async (tx) => { const strat = await secretRotationDAL.deleteById(rotationId, tx); - await secretRotationQueue.removeFromQueue(strat.id, strat.interval); return strat; }); + await secretRotationQueue.removeFromQueue(deletedDoc.id, deletedDoc.interval); return { ...doc, ...deletedDoc }; }; diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index 4092bf356..a16b4548d 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -21,7 +21,7 @@ export const snapshotDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const data = await (tx || db)(TableName.Snapshot) + const data = await (tx || db.replicaNode())(TableName.Snapshot) .where(`${TableName.Snapshot}.id`, id) .join(TableName.Environment, `${TableName.Snapshot}.envId`, `${TableName.Environment}.id`) .select(selectAllTableCols(TableName.Snapshot)) @@ -43,7 +43,7 @@ export const snapshotDALFactory = (db: TDbClient) => { const countOfSnapshotsByFolderId = async (folderId: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.Snapshot) + const doc = await (tx || db.replicaNode())(TableName.Snapshot) .where({ folderId }) .groupBy(["folderId"]) .count("folderId") @@ -56,7 +56,7 @@ export const snapshotDALFactory = (db: TDbClient) => { const findSecretSnapshotDataById = async (snapshotId: string, tx?: Knex) => { try { - const data = await (tx || db)(TableName.Snapshot) + const data = await (tx || db.replicaNode())(TableName.Snapshot) .where(`${TableName.Snapshot}.id`, snapshotId) .join(TableName.Environment, `${TableName.Snapshot}.envId`, `${TableName.Environment}.id`) .leftJoin(TableName.SnapshotSecret, `${TableName.Snapshot}.id`, `${TableName.SnapshotSecret}.snapshotId`) @@ -309,7 +309,7 @@ export const snapshotDALFactory = (db: TDbClient) => { // when we need to rollback we will pull from these snapshots const findLatestSnapshotByFolderId = async (folderId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.Snapshot) + const docs = await (tx || db.replicaNode())(TableName.Snapshot) .where(`${TableName.Snapshot}.folderId`, folderId) .join( (tx || db)(TableName.Snapshot).groupBy("folderId").max("createdAt").select("folderId").as("latestVersion"), diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d768066fa..de0a1d4c2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -42,6 +42,13 @@ export const IDENTITIES = { }, DELETE: { identityId: "The ID of the identity to delete." + }, + GET_BY_ID: { + identityId: "The ID of the identity to get details.", + orgId: "The ID of the org of the identity" + }, + LIST: { + orgId: "The ID of the organization to list identities." } } as const; @@ -65,6 +72,9 @@ export const UNIVERSAL_AUTH = { RETRIEVE: { identityId: "The ID of the identity to retrieve." }, + REVOKE: { + identityId: "The ID of the identity to revoke." + }, 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.", @@ -83,6 +93,10 @@ export const UNIVERSAL_AUTH = { LIST_CLIENT_SECRETS: { identityId: "The ID of the identity to list client secrets for." }, + GET_CLIENT_SECRET: { + identityId: "The ID of the identity to get the client secret from.", + clientSecretId: "The ID of the client secret to get details." + }, REVOKE_CLIENT_SECRET: { identityId: "The ID of the identity to revoke the client secret from.", clientSecretId: "The ID of the client secret to revoke." @@ -104,6 +118,27 @@ export const AWS_AUTH = { iamRequestBody: "The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.", iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request." + }, + REVOKE: { + identityId: "The ID of the identity to revoke." + } +} as const; + +export const AZURE_AUTH = { + REVOKE: { + identityId: "The ID of the identity to revoke." + } +} as const; + +export const GCP_AUTH = { + REVOKE: { + identityId: "The ID of the identity to revoke." + } +} as const; + +export const KUBERNETES_AUTH = { + REVOKE: { + identityId: "The ID of the identity to revoke." } } as const; @@ -347,6 +382,7 @@ export const RAW_SECRETS = { tagIds: "The ID of the tags to be attached to the created secret." }, GET: { + expand: "Whether or not to expand secret references", secretName: "The name of the secret to get.", workspaceId: "The ID of the project to get the secret from.", workspaceSlug: "The slug of the project to get the secret from.", @@ -656,6 +692,7 @@ export const INTEGRATION_AUTH = { integration: "The slug of integration for the auth object.", accessId: "The unique authorized access id of the external integration provider.", accessToken: "The unique authorized access token of the external integration provider.", + awsAssumeIamRoleArn: "The AWS IAM Role to be assumed by Infisical", url: "", namespace: "", refreshToken: "The refresh token for integration authorization." @@ -804,6 +841,8 @@ export const CERTIFICATE_AUTHORITIES = { caId: "The ID of the CA to issue the certificate from", friendlyName: "A friendly name for the certificate", commonName: "The common name (CN) for the certificate", + altNames: + "A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.", ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...", notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format", notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format", diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index f4da71293..90f04d952 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -5,14 +5,25 @@ import { zpStr } from "../zod"; export const GITLAB_URL = "https://gitlab.com"; +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -- If `process.pkg` is set, and it's true, then it means that the app is currently running in a packaged environment (a binary) +export const IS_PACKAGED = (process as any)?.pkg !== undefined; + const zodStrBool = z .enum(["true", "false"]) .optional() .transform((val) => val === "true"); +const databaseReadReplicaSchema = z + .object({ + DB_CONNECTION_URI: z.string().describe("Postgres read replica database connection string"), + DB_ROOT_CERT: zpStr(z.string().optional().describe("Postgres read replica database certificate string")) + }) + .array() + .optional(); + const envSchema = z .object({ - PORT: z.coerce.number().default(4000), + PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000), DISABLE_SECRET_SCANNING: z .enum(["true", "false"]) .default("false") @@ -29,6 +40,7 @@ const envSchema = z 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()), + DB_READ_REPLICAS: zpStr(z.string().describe("Postgres read replicas").optional()), BCRYPT_SALT_ROUND: z.number().default(12), NODE_ENV: z.enum(["development", "test", "production"]).default("production"), SALT_ROUNDS: z.coerce.number().default(10), @@ -101,6 +113,9 @@ const envSchema = z // azure CLIENT_ID_AZURE: zpStr(z.string().optional()), CLIENT_SECRET_AZURE: zpStr(z.string().optional()), + // aws + CLIENT_ID_AWS_INTEGRATION: zpStr(z.string().optional()), + CLIENT_SECRET_AWS_INTEGRATION: zpStr(z.string().optional()), // gitlab CLIENT_ID_GITLAB: zpStr(z.string().optional()), CLIENT_SECRET_GITLAB: zpStr(z.string().optional()), @@ -119,19 +134,24 @@ const envSchema = z // GENERIC STANDALONE_MODE: z .enum(["true", "false"]) - .transform((val) => val === "true") + .transform((val) => val === "true" || IS_PACKAGED) .optional(), INFISICAL_CLOUD: zodStrBool.default("false"), MAINTENANCE_MODE: zodStrBool.default("false"), - CAPTCHA_SECRET: zpStr(z.string().optional()) + CAPTCHA_SECRET: zpStr(z.string().optional()), + PLAIN_API_KEY: zpStr(z.string().optional()), + PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()) }) .transform((data) => ({ ...data, + DB_READ_REPLICAS: data.DB_READ_REPLICAS + ? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS)) + : undefined, isCloud: Boolean(data.LICENSE_SERVER_KEY), isSmtpConfigured: Boolean(data.SMTP_HOST), isRedisConfigured: Boolean(data.REDIS_URL), isDevelopmentMode: data.NODE_ENV === "development", - isProductionMode: data.NODE_ENV === "production", + isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isSecretScanningConfigured: Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index 8d7ea656a..29f716306 100644 --- a/backend/src/lib/crypto/srp.ts +++ b/backend/src/lib/crypto/srp.ts @@ -101,33 +101,51 @@ export const getUserPrivateKey = async ( password: string, user: Pick< TUserEncryptionKeys, - "protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag" + | "protectedKeyTag" + | "protectedKey" + | "protectedKeyIV" + | "encryptedPrivateKey" + | "iv" + | "salt" + | "tag" + | "encryptionVersion" > ) => { - 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 = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: user.protectedKey as string, - iv: user.protectedKeyIV as string, - tag: user.protectedKeyTag as string, - key: derivedKey - }); + if (user.encryptionVersion === 1) { + return decryptSymmetric128BitHexKeyUTF8({ + ciphertext: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag, + key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0") + }); + } + if (user.encryptionVersion === 2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag) { + 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 = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: user.protectedKey, + iv: user.protectedKeyIV, + tag: user.protectedKeyTag, + key: derivedKey + }); - const privateKey = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag, - key: Buffer.from(key, "hex") - }); - return privateKey; + const privateKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag, + key: Buffer.from(key, "hex") + }); + return privateKey; + } + throw new Error(`GetUserPrivateKey: Encryption version not found`); }; export const buildUserProjectKey = async (privateKey: string, publickey: string) => { diff --git a/backend/src/lib/fn/argv.ts b/backend/src/lib/fn/argv.ts new file mode 100644 index 000000000..174573414 --- /dev/null +++ b/backend/src/lib/fn/argv.ts @@ -0,0 +1 @@ +export const isMigrationMode = () => !!process.argv.slice(2).find((arg) => arg === "migration:latest"); // example -> ./binary migration:latest diff --git a/backend/src/lib/fn/index.ts b/backend/src/lib/fn/index.ts index 0d0f07e45..381ecebf3 100644 --- a/backend/src/lib/fn/index.ts +++ b/backend/src/lib/fn/index.ts @@ -1,6 +1,7 @@ // Some of the functions are taken from https://github.com/rayepps/radash // Full credits goes to https://github.com/rayapps to those functions // Code taken to keep in in house and to adjust somethings for our needs +export * from "./argv"; export * from "./array"; export * from "./dates"; export * from "./object"; diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index 0faeba290..bd103af9d 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -50,7 +50,7 @@ export const ormify = (db: Kne }), findById: async (id: string, tx?: Knex) => { try { - const result = await (tx || db)(tableName) + const result = await (tx || db.replicaNode())(tableName) .where({ id } as never) .first("*"); return result; @@ -60,7 +60,7 @@ export const ormify = (db: Kne }, findOne: async (filter: Partial, tx?: Knex) => { try { - const res = await (tx || db)(tableName).where(filter).first("*"); + const res = await (tx || db.replicaNode())(tableName).where(filter).first("*"); return res; } catch (error) { throw new DatabaseError({ error, name: "Find one" }); @@ -71,7 +71,7 @@ export const ormify = (db: Kne { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db)(tableName).where(buildFindFilter(filter)); + const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter)); if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index 5d1a63fc8..942efc40a 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -58,7 +58,8 @@ const redactedKeys = [ "decryptedSecret", "secrets", "key", - "password" + "password", + "config" ]; export const initLogger = async () => { diff --git a/backend/src/main.ts b/backend/src/main.ts index 86681ef33..a1c5dfd09 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,8 +1,10 @@ import dotenv from "dotenv"; +import path from "path"; import { initDbConnection } from "./db"; import { keyStoreFactory } from "./keystore/keystore"; -import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; +import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; +import { isMigrationMode } from "./lib/fn"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -10,20 +12,43 @@ import { bootstrapCheck } from "./server/boot-strap-check"; import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); + const run = async () => { const logger = await initLogger(); const appCfg = initEnvConfig(logger); const db = initDbConnection({ dbConnectionUri: appCfg.DB_CONNECTION_URI, - dbRootCert: appCfg.DB_ROOT_CERT + dbRootCert: appCfg.DB_ROOT_CERT, + readReplicas: appCfg.DB_READ_REPLICAS?.map((el) => ({ + dbRootCert: el.DB_ROOT_CERT, + dbConnectionUri: el.DB_CONNECTION_URI + })) }); + // Case: App is running in packaged mode (binary), and migration mode is enabled. + // Run the migrations and exit the process after completion. + if (IS_PACKAGED && isMigrationMode()) { + try { + logger.info("Running Postgres migrations.."); + await db.migrate.latest({ + directory: path.join(__dirname, "./db/migrations") + }); + logger.info("Postgres migrations completed"); + } catch (err) { + logger.error(err, "Failed to run migrations"); + process.exit(1); + } + + process.exit(0); + } + const smtp = smtpServiceFactory(formatSmtpConfig()); const queue = queueServiceFactory(appCfg.REDIS_URL); const keyStore = keyStoreFactory(appCfg.REDIS_URL); const server = await main({ db, smtp, logger, queue, keyStore }); const bootstrap = await bootstrapCheck({ db }); + // eslint-disable-next-line process.on("SIGINT", async () => { await server.close(); diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 863162c2b..ee8acec0f 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -15,7 +15,7 @@ import { Knex } from "knex"; import { Logger } from "pino"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; import { TSmtpService } from "@app/services/smtp/smtp-service"; @@ -80,8 +80,8 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => { if (appCfg.isProductionMode) { await server.register(registerExternalNextjs, { - standaloneMode: appCfg.STANDALONE_MODE, - dir: path.join(__dirname, "../../"), + standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED, + dir: path.join(__dirname, IS_PACKAGED ? "../../../" : "../../"), port: appCfg.PORT }); } diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index ad54a151a..79b709ee6 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -82,3 +82,9 @@ export const publicSecretShareCreationLimit: RateLimitOptions = { max: 5, keyGenerator: (req) => req.realIp }; + +export const userEngagementLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + max: 5, + keyGenerator: (req) => req.realIp +}; diff --git a/backend/src/server/plugins/external-nextjs.ts b/backend/src/server/plugins/external-nextjs.ts index cc2fe371d..754817035 100644 --- a/backend/src/server/plugins/external-nextjs.ts +++ b/backend/src/server/plugins/external-nextjs.ts @@ -1,9 +1,10 @@ // this plugins allows to run infisical in standalone mode // standalone mode = infisical backend and nextjs frontend in one server // this way users don't need to deploy two things - import path from "node:path"; +import { IS_PACKAGED } from "@app/lib/config/env"; + // to enabled this u need to set standalone mode to true export const registerExternalNextjs = async ( server: FastifyZodProvider, @@ -18,20 +19,33 @@ export const registerExternalNextjs = async ( } ) => { if (standaloneMode) { - const nextJsBuildPath = path.join(dir, "frontend-build"); + const frontendName = IS_PACKAGED ? "frontend" : "frontend-build"; + const nextJsBuildPath = path.join(dir, frontendName); const { default: conf } = (await import( - path.join(dir, "frontend-build/.next/required-server-files.json"), + path.join(dir, `${frontendName}/.next/required-server-files.json`), // @ts-expect-error type { assert: { type: "json" } } )) as { default: { config: string } }; - /* eslint-disable */ - const { default: NextServer } = ( - await import(path.join(dir, "frontend-build/node_modules/next/dist/server/next-server.js")) - ).default; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let NextServer: any; + + if (!IS_PACKAGED) { + /* eslint-disable */ + const { default: nextServer } = ( + await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`)) + ).default; + + NextServer = nextServer; + } else { + /* eslint-disable */ + const nextServer = await import(path.join(dir, `${frontendName}/node_modules/next/dist/server/next-server.js`)); + + NextServer = nextServer.default; + } const nextApp = new NextServer({ dev: false, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 326fbafa6..bc8b153e0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -32,6 +32,8 @@ import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-conf import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal"; import { licenseDALFactory } from "@app/ee/services/license/license-dal"; import { licenseServiceFactory } from "@app/ee/services/license/license-service"; +import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; +import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service"; import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; @@ -162,6 +164,7 @@ import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-servi 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 { userEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { webhookDALFactory } from "@app/services/webhook/webhook-dal"; import { webhookServiceFactory } from "@app/services/webhook/webhook-service"; @@ -250,6 +253,7 @@ export const registerRoutes = async ( const ldapConfigDAL = ldapConfigDALFactory(db); const ldapGroupMapDAL = ldapGroupMapDALFactory(db); + const oidcConfigDAL = oidcConfigDALFactory(db); const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db); const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db); const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); @@ -316,7 +320,6 @@ export const registerRoutes = async ( auditLogStreamDAL }); const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({ - projectMembershipDAL, projectEnvDAL, secretApprovalPolicyApproverDAL: sapApproverDAL, permissionService, @@ -392,7 +395,9 @@ export const registerRoutes = async ( userDAL, userAliasDAL, permissionService, - licenseService + licenseService, + tokenService, + smtpService }); const telemetryService = telemetryServiceFactory({ @@ -410,8 +415,10 @@ export const registerRoutes = async ( userAliasDAL, orgMembershipDAL, tokenService, - smtpService + smtpService, + projectMembershipDAL }); + const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL }); const passwordService = authPaswordServiceFactory({ tokenService, @@ -761,7 +768,6 @@ export const registerRoutes = async ( secretApprovalRequestDAL, secretApprovalRequestSecretDAL, secretQueueService, - projectMembershipDAL, projectBotService }); const secretRotationQueue = secretRotationQueueFactory({ @@ -801,7 +807,8 @@ export const registerRoutes = async ( const identityService = identityServiceFactory({ permissionService, identityDAL, - identityOrgMembershipDAL + identityOrgMembershipDAL, + licenseService }); const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL, @@ -903,6 +910,23 @@ export const registerRoutes = async ( secretSharingDAL }); + const oidcService = oidcConfigServiceFactory({ + orgDAL, + orgMembershipDAL, + userDAL, + userAliasDAL, + licenseService, + tokenService, + smtpService, + orgBotDAL, + permissionService, + oidcConfigDAL + }); + + const userEngagementService = userEngagementServiceFactory({ + userDAL + }); + await superAdminService.initServerCfg(); // // setup the communication with license key server @@ -923,6 +947,7 @@ export const registerRoutes = async ( permission: permissionService, org: orgService, orgRole: orgRoleService, + oidc: oidcService, apiKey: apiKeyService, authToken: tokenService, superAdmin: superAdminService, @@ -973,7 +998,8 @@ export const registerRoutes = async ( telemetry: telemetryService, projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, - secretSharing: secretSharingService + secretSharing: secretSharingService, + userEngagement: userEngagementService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 97c3449d9..6e41df946 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -8,6 +8,7 @@ import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerAdminRouter = async (server: FastifyZodProvider) => { @@ -22,6 +23,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { 200: z.object({ config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({ isMigrationModeOn: z.boolean(), + defaultAuthOrgSlug: z.string().nullable(), isSecretScanningDisabled: z.boolean() }) }) @@ -51,11 +53,22 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { allowSignUp: z.boolean().optional(), allowedSignUpDomain: z.string().optional().nullable(), trustSamlEmails: z.boolean().optional(), - trustLdapEmails: z.boolean().optional() + trustLdapEmails: z.boolean().optional(), + trustOidcEmails: z.boolean().optional(), + defaultAuthOrgId: z.string().optional().nullable(), + enabledLoginMethods: z + .nativeEnum(LoginMethod) + .array() + .optional() + .refine((methods) => !methods || methods.length > 0, { + message: "At least one login method should be enabled." + }) }), response: { 200: z.object({ - config: SuperAdminSchema + config: SuperAdminSchema.extend({ + defaultAuthOrgSlug: z.string().nullable() + }) }) } }, @@ -65,11 +78,87 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const config = await server.services.superAdmin.updateServerCfg(req.body); + const config = await server.services.superAdmin.updateServerCfg(req.body, req.permission.id); return { config }; } }); + server.route({ + method: "GET", + url: "/user-management/users", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + searchTerm: z.string().default(""), + offset: z.coerce.number().default(0), + limit: z.coerce.number().max(100).default(20) + }), + response: { + 200: z.object({ + users: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true + }).array() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const users = await server.services.superAdmin.getUsers({ + ...req.query + }); + + return { + users + }; + } + }); + + server.route({ + method: "DELETE", + url: "/user-management/users/:userId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + userId: z.string() + }), + response: { + 200: z.object({ + users: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true + }) + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const users = await server.services.superAdmin.deleteUser(req.params.userId); + + return { + users + }; + } + }); + server.route({ method: "POST", url: "/signup", diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 7573c0bd2..533f249f2 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -9,7 +9,10 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; -import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; +import { + validateAltNamesField, + validateCaDateField +} from "@app/services/certificate-authority/certificate-authority-validators"; export const registerCaRouter = async (server: FastifyZodProvider) => { server.route({ @@ -452,6 +455,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { .object({ friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), + altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), ttl: z .string() .refine((val) => ms(val) > 0, "TTL must be a positive number") diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index f8c045168..8a85323a6 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -266,4 +266,51 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) return { identityAwsAuth }; } }); + + server.route({ + method: "DELETE", + url: "/aws-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete AWS Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(AWS_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityAwsAuth: IdentityAwsAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAwsAuth = await server.services.identityAwsAuth.revokeIdentityAwsAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAwsAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_AWS_AUTH, + metadata: { + identityId: identityAwsAuth.identityId + } + } + }); + + return { identityAwsAuth }; + } + }); }; diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index d10cd131b..6b4a7fb37 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IdentityAzureAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { AZURE_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -259,4 +260,51 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider return { identityAzureAuth }; } }); + + server.route({ + method: "DELETE", + url: "/azure-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete Azure Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(AZURE_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityAzureAuth: IdentityAzureAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAzureAuth = await server.services.identityAzureAuth.revokeIdentityAzureAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAzureAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_AZURE_AUTH, + metadata: { + identityId: identityAzureAuth.identityId + } + } + }); + + return { identityAzureAuth }; + } + }); }; diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index 34940eb13..0deeb95d3 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IdentityGcpAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { GCP_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -265,4 +266,51 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) return { identityGcpAuth }; } }); + + server.route({ + method: "DELETE", + url: "/gcp-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete GCP Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(GCP_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityGcpAuth: IdentityGcpAuthsSchema + }) + } + }, + handler: async (req) => { + const identityGcpAuth = await server.services.identityGcpAuth.revokeIdentityGcpAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityGcpAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_GCP_AUTH, + metadata: { + identityId: identityGcpAuth.identityId + } + } + }); + + return { identityGcpAuth }; + } + }); }; diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 227345916..4c54f1e7c 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IdentityKubernetesAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { KUBERNETES_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -280,4 +281,54 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide return { identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.parse(identityKubernetesAuth) }; } }); + + server.route({ + method: "DELETE", + url: "/kubernetes-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete Kubernetes Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(KUBERNETES_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.omit({ + caCert: true, + tokenReviewerJwt: true + }) + }) + } + }, + handler: async (req) => { + const identityKubernetesAuth = await server.services.identityKubernetesAuth.revokeIdentityKubernetesAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityKubernetesAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_KUBERNETES_AUTH, + metadata: { + identityId: identityKubernetesAuth.identityId + } + } + }); + + return { identityKubernetesAuth }; + } + }); }; diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index e174cf974..a425f963e 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -1,9 +1,9 @@ import { z } from "zod"; -import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas"; +import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { IDENTITIES } from "@app/lib/api-docs"; -import { creationLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { creationLimit, readLimit, writeLimit } 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"; @@ -170,4 +170,94 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { return { identity }; } }); + + server.route({ + method: "GET", + url: "/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get an identity by id", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(IDENTITIES.GET_BY_ID.identityId) + }), + response: { + 200: z.object({ + identity: IdentityOrgMembershipsSchema.extend({ + customRole: OrgRolesSchema.pick({ + id: true, + name: true, + slug: true, + permissions: true, + description: true + }).optional(), + identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) + }) + }) + } + }, + handler: async (req) => { + const identity = await server.services.identity.getIdentityById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.identityId + }); + + return { identity }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "List identities", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + orgId: z.string().describe(IDENTITIES.LIST.orgId) + }), + response: { + 200: z.object({ + identities: IdentityOrgMembershipsSchema.extend({ + customRole: OrgRolesSchema.pick({ + id: true, + name: true, + slug: true, + permissions: true, + description: true + }).optional(), + identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) + }).array() + }) + } + }, + handler: async (req) => { + const identities = await server.services.identity.listOrgIdentities({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + orgId: req.query.orgId + }); + + return { identities }; + } + }); }; diff --git a/backend/src/server/routes/v1/identity-ua.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts similarity index 81% rename from backend/src/server/routes/v1/identity-ua.ts rename to backend/src/server/routes/v1/identity-universal-auth-router.ts index 670f52416..b5a63f0db 100644 --- a/backend/src/server/routes/v1/identity-ua.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -134,7 +134,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const identityUniversalAuth = await server.services.identityUa.attachUa({ + const identityUniversalAuth = await server.services.identityUa.attachUniversalAuth({ actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, @@ -219,7 +219,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const identityUniversalAuth = await server.services.identityUa.updateUa({ + const identityUniversalAuth = await server.services.identityUa.updateUniversalAuth({ actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, @@ -272,7 +272,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const identityUniversalAuth = await server.services.identityUa.getIdentityUa({ + const identityUniversalAuth = await server.services.identityUa.getIdentityUniversalAuth({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -295,6 +295,53 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "DELETE", + url: "/universal-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete Universal Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(UNIVERSAL_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityUniversalAuth: IdentityUniversalAuthsSchema + }) + } + }, + handler: async (req) => { + const identityUniversalAuth = await server.services.identityUa.revokeIdentityUniversalAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityUniversalAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityUniversalAuth.identityId + } + } + }); + + return { identityUniversalAuth }; + } + }); + server.route({ method: "POST", url: "/universal-auth/identities/:identityId/client-secrets", @@ -325,14 +372,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - ...req.body - }); + const { clientSecret, clientSecretData, orgId } = + await server.services.identityUa.createUniversalAuthClientSecret({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + ...req.body + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -374,13 +422,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId - }); + const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUniversalAuthClientSecrets( + { + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + } + ); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -396,6 +446,56 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get Universal Auth Client Secret for identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.identityId), + clientSecretId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.clientSecretId) + }), + response: { + 200: z.object({ + clientSecretData: sanitizedClientSecretSchema + }) + } + }, + handler: async (req) => { + const clientSecretData = await server.services.identityUa.getUniversalAuthClientSecretById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + clientSecretId: req.params.clientSecretId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: clientSecretData.orgId, + event: { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: clientSecretData.identityId, + clientSecretId: clientSecretData.id + } + } + }); + + return { clientSecretData }; + } + }); + server.route({ method: "POST", url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke", @@ -421,7 +521,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const clientSecretData = await server.services.identityUa.revokeUaClientSecret({ + const clientSecretData = await server.services.identityUa.revokeUniversalAuthClientSecret({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index eee7dac65..d5c5acbcf 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -9,7 +9,7 @@ import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityRouter } from "./identity-router"; -import { registerIdentityUaRouter } from "./identity-ua"; +import { registerIdentityUaRouter } from "./identity-universal-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationRouter } from "./integration-router"; import { registerInviteOrgRouter } from "./invite-org-router"; @@ -25,6 +25,7 @@ import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretTagRouter } from "./secret-tag-router"; import { registerSsoRouter } from "./sso-router"; import { registerUserActionRouter } from "./user-action-router"; +import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserRouter } from "./user-router"; import { registerWebhookRouter } from "./webhook-router"; @@ -77,4 +78,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerWebhookRouter, { prefix: "/webhooks" }); await server.register(registerIdentityRouter, { prefix: "/identities" }); await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" }); + await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); }; diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 899c1cac8..963b7101c 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -240,6 +240,12 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) integration: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration), accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId), accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken), + awsAssumeIamRoleArn: z + .string() + .url() + .trim() + .optional() + .describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.awsAssumeIamRoleArn), url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url), namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace), refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken) diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 0984b66f6..99f05cf94 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -372,6 +372,44 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "PUT", + url: "/:workspaceSlug/audit-logs-retention", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceSlug: z.string().trim() + }), + body: z.object({ + auditLogsRetentionDays: z.number().min(0) + }), + response: { + 200: z.object({ + message: z.string(), + workspace: ProjectsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const workspace = await server.services.project.updateAuditLogsRetention({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + workspaceSlug: req.params.workspaceSlug, + auditLogsRetentionDays: req.body.auditLogsRetentionDays + }); + + return { + message: "Successfully updated project's audit logs retention period", + workspace + }; + } + }); + server.route({ method: "GET", url: "/:workspaceId/integrations", diff --git a/backend/src/server/routes/v1/user-engagement-router.ts b/backend/src/server/routes/v1/user-engagement-router.ts new file mode 100644 index 000000000..e3ce6532e --- /dev/null +++ b/backend/src/server/routes/v1/user-engagement-router.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +import { userEngagementLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerUserEngagementRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/me/wish", + config: { + rateLimit: userEngagementLimit + }, + schema: { + body: z.object({ + text: z.string().min(1) + }), + response: { + 200: z.object({}) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + return server.services.userEngagement.createUserWish(req.permission.id, req.body.text); + } + }); +}; diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index b9269d66e..d3c0db242 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; -import { authRateLimit, readLimit } from "@app/server/config/rateLimiter"; +import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -90,4 +90,48 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { return res.redirect(`${appCfg.SITE_URL}/login`); } }); + + server.route({ + method: "GET", + url: "/me/project-favorites", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + orgId: z.string().trim() + }), + response: { + 200: z.object({ + projectFavorites: z.string().array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + return server.services.user.getUserProjectFavorites(req.permission.id, req.query.orgId); + } + }); + + server.route({ + method: "PUT", + url: "/me/project-favorites", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + orgId: z.string().trim(), + projectFavorites: z.string().array() + }) + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + return server.services.user.updateUserProjectFavorites( + req.permission.id, + req.body.orgId, + req.body.projectFavorites + ); + } + }); }; diff --git a/backend/src/server/routes/v1/webhook-router.ts b/backend/src/server/routes/v1/webhook-router.ts index 1698c0c4b..7423f9f3e 100644 --- a/backend/src/server/routes/v1/webhook-router.ts +++ b/backend/src/server/routes/v1/webhook-router.ts @@ -6,13 +6,17 @@ import { removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { WebhookType } from "@app/services/webhook/webhook-types"; export const sanitizedWebhookSchema = WebhooksSchema.omit({ encryptedSecretKey: true, iv: true, tag: true, algorithm: true, - keyEncoding: true + keyEncoding: true, + urlCipherText: true, + urlIV: true, + urlTag: true }).merge( z.object({ projectId: z.string(), @@ -33,13 +37,24 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), schema: { - body: z.object({ - workspaceId: z.string().trim(), - environment: z.string().trim(), - webhookUrl: z.string().url().trim(), - webhookSecretKey: z.string().trim().optional(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash) - }), + body: z + .object({ + type: z.nativeEnum(WebhookType).default(WebhookType.GENERAL), + workspaceId: z.string().trim(), + environment: z.string().trim(), + webhookUrl: z.string().url().trim(), + webhookSecretKey: z.string().trim().optional(), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash) + }) + .superRefine((data, ctx) => { + if (data.type === WebhookType.SLACK && !data.webhookUrl.includes("hooks.slack.com")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Incoming Webhook URL is invalid.", + path: ["webhookUrl"] + }); + } + }), response: { 200: z.object({ message: z.string(), @@ -66,8 +81,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { environment: webhook.environment.slug, webhookId: webhook.id, isDisabled: webhook.isDisabled, - secretPath: webhook.secretPath, - webhookUrl: webhook.url + secretPath: webhook.secretPath } } }); @@ -116,8 +130,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { environment: webhook.environment.slug, webhookId: webhook.id, isDisabled: webhook.isDisabled, - secretPath: webhook.secretPath, - webhookUrl: webhook.url + secretPath: webhook.secretPath } } }); @@ -156,8 +169,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { environment: webhook.environment.slug, webhookId: webhook.id, isDisabled: webhook.isDisabled, - secretPath: webhook.secretPath, - webhookUrl: webhook.url + secretPath: webhook.secretPath } } }); diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 21dd32021..01c7eda6d 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -297,7 +297,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const user = await server.services.user.deleteMe(req.permission.id); + const user = await server.services.user.deleteUser(req.permission.id); return { user }; } }); diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index e3f1528ad..910cc3db9 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -300,6 +300,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { 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), + expandSecretReferences: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.GET.expand), include_imports: z .enum(["true", "false"]) .default("false") @@ -344,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, + expandSecretReferences: req.query.expandSecretReferences, environment, projectId: workspaceId, projectSlug: workspaceSlug, @@ -943,7 +949,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { event: { type: EventType.SECRET_APPROVAL_REQUEST, metadata: { - committedBy: approval.committerId, + committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, secretApprovalRequestSlug: approval.slug } @@ -1127,7 +1133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { event: { type: EventType.SECRET_APPROVAL_REQUEST, metadata: { - committedBy: approval.committerId, + committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, secretApprovalRequestSlug: approval.slug } @@ -1265,7 +1271,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { event: { type: EventType.SECRET_APPROVAL_REQUEST, metadata: { - committedBy: approval.committerId, + committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, secretApprovalRequestSlug: approval.slug } @@ -1391,7 +1397,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { event: { type: EventType.SECRET_APPROVAL_REQUEST, metadata: { - committedBy: approval.committerId, + committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, secretApprovalRequestSlug: approval.slug } @@ -1518,7 +1524,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { event: { type: EventType.SECRET_APPROVAL_REQUEST, metadata: { - committedBy: approval.committerId, + committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, secretApprovalRequestSlug: approval.slug } @@ -1632,7 +1638,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { event: { type: EventType.SECRET_APPROVAL_REQUEST, metadata: { - committedBy: approval.committerId, + committedBy: approval.committerUserId, secretApprovalRequestId: approval.id, secretApprovalRequestSlug: approval.slug } diff --git a/backend/src/services/auth-token/auth-token-dal.ts b/backend/src/services/auth-token/auth-token-dal.ts index 075ae7384..c058c13e8 100644 --- a/backend/src/services/auth-token/auth-token-dal.ts +++ b/backend/src/services/auth-token/auth-token-dal.ts @@ -14,7 +14,7 @@ export const tokenDALFactory = (db: TDbClient) => { const findOneTokenSession = async (filter: Partial): Promise => { try { - const doc = await db(TableName.AuthTokenSession).where(filter).first(); + const doc = await db.replicaNode()(TableName.AuthTokenSession).where(filter).first(); return doc; } catch (error) { throw new DatabaseError({ error, name: "FindOneTokenSession" }); @@ -44,7 +44,7 @@ export const tokenDALFactory = (db: TDbClient) => { const findTokenSessions = async (filter: Partial, tx?: Knex) => { try { - const sessions = await (tx || db)(TableName.AuthTokenSession).where(filter); + const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter); return sessions; } catch (error) { throw new DatabaseError({ name: "Find all token session", error }); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 29a2a176f..2ce900b47 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -9,6 +9,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TTokenDALFactory } from "../auth-token/auth-token-dal"; @@ -16,6 +17,7 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TOrgDALFactory } from "../org/org-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { LoginMethod } from "../super-admin/super-admin-types"; import { TUserDALFactory } from "../user/user-dal"; import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; import { @@ -157,9 +159,22 @@ export const authLoginServiceFactory = ({ const userEnc = await userDAL.findUserEncKeyByUsername({ username: email }); + const serverCfg = await getServerCfg(); + + if ( + serverCfg.enabledLoginMethods && + !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && + !providerAuthToken + ) { + throw new BadRequestError({ + message: "Login with email is disabled by administrator." + }); + } + if (!userEnc || (userEnc && !userEnc.isAccepted)) { throw new Error("Failed to find user"); } + if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { validateProviderAuthToken(providerAuthToken as string, email); } @@ -201,7 +216,10 @@ export const authLoginServiceFactory = ({ const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); authMethod = decodedProviderToken.authMethod; - if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId) { + if ( + (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) && + decodedProviderToken.orgId + ) { organizationId = decodedProviderToken.orgId; } } @@ -258,7 +276,13 @@ export const authLoginServiceFactory = ({ }); // from password decrypt the private key if (password) { - const privateKey = await getUserPrivateKey(password, userEnc); + const privateKey = await getUserPrivateKey(password, userEnc).catch((err) => { + logger.error( + err, + `loginExchangeClientProof: private key generation failed for [userId=${user.id}] and [email=${user.email}] ` + ); + return ""; + }); const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey); await userDAL.updateUserEncryptionByUserId(userEnc.userId, { @@ -344,9 +368,12 @@ export const authLoginServiceFactory = ({ // Check if the user actually has access to the specified organization. const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId); + const selectedOrg = await orgDAL.findById(organizationId); if (!hasOrganizationMembership) { - throw new UnauthorizedError({ message: "User does not have access to the organization" }); + throw new UnauthorizedError({ + message: `User does not have access to the organization named ${selectedOrg?.name}` + }); } await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId); @@ -494,6 +521,40 @@ export const authLoginServiceFactory = ({ let user = await userDAL.findUserByUsername(email); const serverCfg = await getServerCfg(); + if (serverCfg.enabledLoginMethods) { + switch (authMethod) { + case AuthMethod.GITHUB: { + if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { + throw new BadRequestError({ + message: "Login with Github is disabled by administrator.", + name: "Oauth 2 login" + }); + } + break; + } + case AuthMethod.GOOGLE: { + if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) { + throw new BadRequestError({ + message: "Login with Google is disabled by administrator.", + name: "Oauth 2 login" + }); + } + break; + } + case AuthMethod.GITLAB: { + if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) { + throw new BadRequestError({ + message: "Login with Gitlab is disabled by administrator.", + name: "Oauth 2 login" + }); + } + break; + } + default: + break; + } + } + const appCfg = getConfig(); if (!user) { @@ -571,7 +632,8 @@ export const authLoginServiceFactory = ({ const { authMethod, userName } = decodedProviderToken; if (!userName) throw new BadRequestError({ message: "Missing user name" }); const organizationId = - (isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId + (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) && + decodedProviderToken.orgId ? decodedProviderToken.orgId : undefined; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 8cf2c9d34..83a5b27d9 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -165,7 +165,8 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag + tag: encryptedPrivateKeyTag, + encryptionVersion: 2 }); const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey); const updateduser = await authDAL.transaction(async (tx) => { @@ -192,7 +193,10 @@ export const authSignupServiceFactory = ({ tx ); // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it - if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) { + if ( + (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) && + organizationId + ) { const [pendingOrgMembership] = await orgDAL.findMembership({ [`${TableName.OrgMembership}.userId` as "userId"]: user.id, status: OrgMembershipStatus.Invited, @@ -325,7 +329,8 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag + tag: encryptedPrivateKeyTag, + encryptionVersion: 2 }); const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey); const updateduser = await authDAL.transaction(async (tx) => { @@ -359,7 +364,7 @@ export const authSignupServiceFactory = ({ tx ); const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; - await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))); + await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx))); await convertPendingGroupAdditionsToGroupMemberships({ userIds: [user.id], diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 8e7b92253..9210093ab 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -8,7 +8,8 @@ export enum AuthMethod { JUMPCLOUD_SAML = "jumpcloud-saml", GOOGLE_SAML = "google-saml", KEYCLOAK_SAML = "keycloak-saml", - LDAP = "ldap" + LDAP = "ldap", + OIDC = "oidc" } export enum AuthTokenType { diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 1b4b30e73..837bbcf37 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -16,6 +16,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { parentCaId?: string; encryptedCertificate: Buffer; }[] = await db + .replicaNode() .withRecursive("cte", (cte) => { void cte .select("ca.id as caId", "ca.parentCaId", "cert.encryptedCertificate") diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 2345180a3..7d87545e2 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import crypto, { KeyObject } from "crypto"; import ms from "ms"; +import { z } from "zod"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -38,6 +39,7 @@ import { TSignIntermediateDTO, TUpdateCaDTO } from "./certificate-authority-types"; +import { hostnameRegex } from "./certificate-authority-validators"; type TCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< @@ -653,6 +655,7 @@ export const certificateAuthorityServiceFactory = ({ caId, friendlyName, commonName, + altNames, ttl, notBefore, notAfter, @@ -738,6 +741,45 @@ export const certificateAuthorityServiceFactory = ({ kmsService }); + const extensions: x509.Extension[] = [ + new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) + ]; + + if (altNames) { + const altNamesArray: { + type: "email" | "dns"; + value: string; + }[] = altNames + .split(",") + .map((name) => name.trim()) + .map((altName) => { + // check if the altName is a valid email + if (z.string().email().safeParse(altName).success) { + return { + type: "email", + value: altName + }; + } + + // check if the altName is a valid hostname + if (hostnameRegex.test(altName)) { + return { + type: "dns", + value: altName + }; + } + + // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly + throw new Error(`Invalid altName: ${altName}`); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + } + const serialNumber = crypto.randomBytes(32).toString("hex"); const leafCert = await x509.X509CertificateGenerator.create({ serialNumber, @@ -748,12 +790,7 @@ export const certificateAuthorityServiceFactory = ({ signingKey: caPrivateKey, publicKey: csrObj.publicKey, signingAlgorithm: alg, - extensions: [ - new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), - new x509.BasicConstraintsExtension(false), - await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) - ] + extensions }); const skLeafObj = KeyObject.from(leafKeys.privateKey); @@ -771,6 +808,7 @@ export const certificateAuthorityServiceFactory = ({ status: CertStatus.ACTIVE, friendlyName: friendlyName || commonName, commonName, + altNames, serialNumber, notBefore: notBeforeDate, notAfter: notAfterDate diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 3ba7624c0..8af8b679c 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -75,6 +75,7 @@ export type TIssueCertFromCaDTO = { caId: string; friendlyName?: string; commonName: string; + altNames: string; ttl: string; notBefore?: string; notAfter?: string; diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 77bf9ad2f..a6d6c8c23 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -6,3 +6,29 @@ const isValidDate = (dateString: string) => { }; export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" }); + +export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/; +export const validateAltNamesField = z + .string() + .trim() + .default("") + .transform((data) => { + if (data === "") return ""; + // Trim each alt name and join with ', ' to ensure formatting + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }) + .refine( + (data) => { + if (data === "") return true; + // Split and validate each alt name + return data.split(", ").every((name) => { + return hostnameRegex.test(name) || z.string().email().safeParse(name).success; + }); + }, + { + message: "Each alt name must be a valid hostname or email address" + } + ); diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index 415bfabf9..67dca3aca 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -14,7 +14,8 @@ export const certificateDALFactory = (db: TDbClient) => { count: string; } - const count = await db(TableName.Certificate) + const count = await db + .replicaNode()(TableName.Certificate) .join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`) .join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.id`, projectId) diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index 3b0523dde..a1d276376 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -12,7 +12,7 @@ export const groupProjectDALFactory = (db: TDbClient) => { const findByProjectId = async (projectId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.GroupProjectMembership) + const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership) .where(`${TableName.GroupProjectMembership}.projectId`, projectId) .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) .join( diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index a0f9fbc27..4f04ef0ac 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -12,7 +12,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { const findOne = async (filter: Partial, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.IdentityAccessToken) + const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken) .where(filter) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .leftJoin(TableName.IdentityUaClientSecret, (qb) => { diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index a58944909..9cb39aece 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -7,11 +7,12 @@ import { IdentityAuthMethod } 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; -import { AuthTokenType } from "../auth/auth-type"; +import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; @@ -24,12 +25,13 @@ import { TGetAwsAuthDTO, TGetCallerIdentityResponse, TLoginAwsAuthDTO, + TRevokeAwsAuthDTO, TUpdateAwsAuthDTO } from "./identity-aws-auth-types"; type TIdentityAwsAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; - identityAwsAuthDAL: Pick; + identityAwsAuthDAL: Pick; identityOrgMembershipDAL: Pick; identityDAL: Pick; licenseService: Pick; @@ -301,10 +303,54 @@ export const identityAwsAuthServiceFactory = ({ return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId }; }; + const revokeIdentityAwsAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeAwsAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH) + throw new BadRequestError({ + message: "The identity does not have aws auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke aws auth of identity with more privileged role" + }); + + const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { + const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityAwsAuth; + }; + return { login, attachAwsAuth, updateAwsAuth, - getAwsAuth + getAwsAuth, + revokeIdentityAwsAuth }; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts index e45783ae1..c24186ee0 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts @@ -52,3 +52,7 @@ export type TGetCallerIdentityResponse = { ResponseMetadata: { RequestId: string }; }; }; + +export type TRevokeAwsAuthDTO = { + identityId: string; +} & Omit; diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index fa439bdc0..0d52bf6a0 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -5,11 +5,12 @@ import { IdentityAuthMethod } 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; -import { AuthTokenType } from "../auth/auth-type"; +import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; @@ -20,11 +21,15 @@ import { TAttachAzureAuthDTO, TGetAzureAuthDTO, TLoginAzureAuthDTO, + TRevokeAzureAuthDTO, TUpdateAzureAuthDTO } from "./identity-azure-auth-types"; type TIdentityAzureAuthServiceFactoryDep = { - identityAzureAuthDAL: Pick; + identityAzureAuthDAL: Pick< + TIdentityAzureAuthDALFactory, + "findOne" | "transaction" | "create" | "updateById" | "delete" + >; identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; identityDAL: Pick; @@ -277,10 +282,54 @@ export const identityAzureAuthServiceFactory = ({ return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; }; + const revokeIdentityAzureAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeAzureAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH) + throw new BadRequestError({ + message: "The identity does not have azure auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke azure auth of identity with more privileged role" + }); + + const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { + const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityAzureAuth; + }; + return { login, attachAzureAuth, updateAzureAuth, - getAzureAuth + getAzureAuth, + revokeIdentityAzureAuth }; }; diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts index 65459003c..ec03451db 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts @@ -118,3 +118,7 @@ export type TDecodedAzureAuthJwt = { [key: string]: string; }; }; + +export type TRevokeAzureAuthDTO = { + identityId: string; +} & Omit; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 5f829cb33..edac7c132 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -5,11 +5,12 @@ import { IdentityAuthMethod } 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; -import { AuthTokenType } from "../auth/auth-type"; +import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; @@ -21,11 +22,12 @@ import { TGcpIdentityDetails, TGetGcpAuthDTO, TLoginGcpAuthDTO, + TRevokeGcpAuthDTO, TUpdateGcpAuthDTO } from "./identity-gcp-auth-types"; type TIdentityGcpAuthServiceFactoryDep = { - identityGcpAuthDAL: Pick; + identityGcpAuthDAL: Pick; identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; identityDAL: Pick; @@ -315,10 +317,54 @@ export const identityGcpAuthServiceFactory = ({ return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; }; + const revokeIdentityGcpAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeGcpAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH) + throw new BadRequestError({ + message: "The identity does not have gcp auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke gcp auth of identity with more privileged role" + }); + + const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { + const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityGcpAuth; + }; + return { login, attachGcpAuth, updateGcpAuth, - getGcpAuth + getGcpAuth, + revokeIdentityGcpAuth }; }; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts index 60ab36b58..45e64b24b 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts @@ -76,3 +76,7 @@ export type TDecodedGcpIamAuthJwt = { [key: string]: string; }; }; + +export type TRevokeGcpAuthDTO = { + identityId: string; +} & Omit; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index f1e1c6be0..820777b46 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -7,6 +7,7 @@ import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric, @@ -16,11 +17,11 @@ import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; -import { AuthTokenType } from "../auth/auth-type"; +import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; @@ -32,13 +33,14 @@ import { TCreateTokenReviewResponse, TGetKubernetesAuthDTO, TLoginKubernetesAuthDTO, + TRevokeKubernetesAuthDTO, TUpdateKubernetesAuthDTO } from "./identity-kubernetes-auth-types"; type TIdentityKubernetesAuthServiceFactoryDep = { identityKubernetesAuthDAL: Pick< TIdentityKubernetesAuthDALFactory, - "create" | "findOne" | "transaction" | "updateById" + "create" | "findOne" | "transaction" | "updateById" | "delete" >; identityAccessTokenDAL: Pick; identityOrgMembershipDAL: Pick; @@ -533,10 +535,54 @@ export const identityKubernetesAuthServiceFactory = ({ return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; }; + const revokeIdentityKubernetesAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeKubernetesAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH) + throw new BadRequestError({ + message: "The identity does not have kubenetes auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke kubenetes auth of identity with more privileged role" + }); + + const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { + const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityKubernetesAuth; + }; + return { login, attachKubernetesAuth, updateKubernetesAuth, - getKubernetesAuth + getKubernetesAuth, + revokeIdentityKubernetesAuth }; }; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts index dbb42dce8..f1cde2be9 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts @@ -59,3 +59,7 @@ export type TCreateTokenReviewResponse = { }; status: TCreateTokenReviewSuccessResponse | TCreateTokenReviewErrorResponse; }; + +export type TRevokeKubernetesAuthDTO = { + identityId: string; +} & Omit; diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index c1cfe79cc..497d05c3c 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -12,7 +12,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.IdentityProjectMembership) + const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) .where((qb) => { diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 5e940871b..00dfa5d60 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -25,7 +25,9 @@ import { TCreateUaClientSecretDTO, TGetUaClientSecretsDTO, TGetUaDTO, + TGetUniversalAuthClientSecretByIdDTO, TRevokeUaClientSecretDTO, + TRevokeUaDTO, TUpdateUaDTO } from "./identity-ua-types"; @@ -136,7 +138,7 @@ export const identityUaServiceFactory = ({ return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg }; }; - const attachUa = async ({ + const attachUniversalAuth = async ({ accessTokenMaxTTL, identityId, accessTokenNumUsesLimit, @@ -227,7 +229,7 @@ export const identityUaServiceFactory = ({ return { ...identityUa, orgId: identityMembershipOrg.orgId }; }; - const updateUa = async ({ + const updateUniversalAuth = async ({ accessTokenMaxTTL, identityId, accessTokenNumUsesLimit, @@ -312,7 +314,7 @@ export const identityUaServiceFactory = ({ return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId }; }; - const getIdentityUa = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { + const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) @@ -333,7 +335,50 @@ export const identityUaServiceFactory = ({ return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; }; - const createUaClientSecret = async ({ + const revokeIdentityUniversalAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeUaDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) + throw new BadRequestError({ + message: "The identity does not have universal auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke universal auth of identity with more privileged role" + }); + + const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { + const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityUniversalAuth; + }; + + const createUniversalAuthClientSecret = async ({ actor, actorId, actorOrgId, @@ -396,7 +441,7 @@ export const identityUaServiceFactory = ({ }; }; - const getUaClientSecrets = async ({ + const getUniversalAuthClientSecrets = async ({ actor, actorId, actorOrgId, @@ -442,7 +487,47 @@ export const identityUaServiceFactory = ({ return { clientSecrets, orgId: identityMembershipOrg.orgId }; }; - const revokeUaClientSecret = async ({ + const getUniversalAuthClientSecretById = async ({ + identityId, + actorId, + actor, + actorOrgId, + actorAuthMethod, + clientSecretId + }: TGetUniversalAuthClientSecretByIdDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral) + throw new BadRequestError({ + message: "The identity does not have universal auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to read identity client secret of project with more privileged role" + }); + + const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId); + return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; + }; + + const revokeUniversalAuthClientSecret = async ({ identityId, actorId, actor, @@ -475,7 +560,7 @@ export const identityUaServiceFactory = ({ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); if (!hasPriviledge) throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" + message: "Failed to revoke identity client secret with more privileged role" }); const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { @@ -486,11 +571,13 @@ export const identityUaServiceFactory = ({ return { login, - attachUa, - updateUa, - getIdentityUa, - createUaClientSecret, - getUaClientSecrets, - revokeUaClientSecret + attachUniversalAuth, + updateUniversalAuth, + getIdentityUniversalAuth, + revokeIdentityUniversalAuth, + createUniversalAuthClientSecret, + getUniversalAuthClientSecrets, + revokeUniversalAuthClientSecret, + getUniversalAuthClientSecretById }; }; diff --git a/backend/src/services/identity-ua/identity-ua-types.ts b/backend/src/services/identity-ua/identity-ua-types.ts index 2cc4762a8..2045c2143 100644 --- a/backend/src/services/identity-ua/identity-ua-types.ts +++ b/backend/src/services/identity-ua/identity-ua-types.ts @@ -22,6 +22,10 @@ export type TGetUaDTO = { identityId: string; } & Omit; +export type TRevokeUaDTO = { + identityId: string; +} & Omit; + export type TCreateUaClientSecretDTO = { identityId: string; description: string; @@ -37,3 +41,8 @@ export type TRevokeUaClientSecretDTO = { identityId: string; clientSecretId: string; } & Omit; + +export type TGetUniversalAuthClientSecretByIdDTO = { + identityId: string; + clientSecretId: string; +} & Omit; diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 95d742f33..104b917e7 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -12,7 +12,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { const findOne = async (filter: Partial, tx?: Knex) => { try { - const [data] = await (tx || db)(TableName.IdentityOrgMembership) + const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) .where(filter) .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) .select(selectAllTableCols(TableName.IdentityOrgMembership)) @@ -27,10 +27,10 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; - const findByOrgId = async (orgId: string, tx?: Knex) => { + const find = async (filter: Partial, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.IdentityOrgMembership) - .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) + const docs = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) + .where(filter) .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .select(selectAllTableCols(TableName.IdentityOrgMembership)) @@ -79,5 +79,5 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; - return { ...identityOrgOrm, findOne, findByOrgId }; + return { ...identityOrgOrm, find, findOne }; }; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 2863bf23e..62c812a3e 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; -import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { OrgMembershipRole, TableName, TOrgRoles } 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; @@ -10,12 +11,13 @@ import { TOrgPermission } from "@app/lib/types"; import { ActorType } from "../auth/auth-type"; import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityOrgDALFactory } from "./identity-org-dal"; -import { TCreateIdentityDTO, TDeleteIdentityDTO, TUpdateIdentityDTO } from "./identity-types"; +import { TCreateIdentityDTO, TDeleteIdentityDTO, TGetIdentityByIdDTO, TUpdateIdentityDTO } from "./identity-types"; type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; permissionService: Pick; + licenseService: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -23,7 +25,8 @@ export type TIdentityServiceFactory = ReturnType; export const identityServiceFactory = ({ identityDAL, identityOrgMembershipDAL, - permissionService + permissionService, + licenseService }: TIdentityServiceFactoryDep) => { const createIdentity = async ({ name, @@ -45,6 +48,14 @@ export const identityServiceFactory = ({ const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" }); + const plan = await licenseService.getPlan(orgId); + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities." + }); + } + const identity = await identityDAL.transaction(async (tx) => { const newIdentity = await identityDAL.create({ name }, tx); await identityOrgMembershipDAL.create( @@ -58,6 +69,7 @@ export const identityServiceFactory = ({ ); return newIdentity; }); + await licenseService.updateSubscriptionOrgMemberCount(orgId); return identity; }; @@ -115,7 +127,7 @@ export const identityServiceFactory = ({ { identityId: id }, { role: customRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + roleId: customRole?.id || null }, tx ); @@ -126,6 +138,24 @@ export const identityServiceFactory = ({ return { ...identity, orgId: identityOrgMembership.orgId }; }; + const getIdentityById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetIdentityByIdDTO) => { + const doc = await identityOrgMembershipDAL.find({ + [`${TableName.IdentityOrgMembership}.identityId` as "identityId"]: id + }); + const identity = doc[0]; + if (!identity) throw new BadRequestError({ message: `Failed to find identity with id ${id}` }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identity.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + return identity; + }; + const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => { const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` }); @@ -150,6 +180,9 @@ export const identityServiceFactory = ({ throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); const deletedIdentity = await identityDAL.deleteById(id); + + await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId); + return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; }; @@ -157,7 +190,9 @@ export const identityServiceFactory = ({ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - const identityMemberships = await identityOrgMembershipDAL.findByOrgId(orgId); + const identityMemberships = await identityOrgMembershipDAL.find({ + [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId + }); return identityMemberships; }; @@ -165,6 +200,7 @@ export const identityServiceFactory = ({ createIdentity, updateIdentity, deleteIdentity, - listOrgIdentities + listOrgIdentities, + getIdentityById }; }; diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index 10b943667..5125413e8 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -16,6 +16,10 @@ export type TDeleteIdentityDTO = { id: string; } & Omit; +export type TGetIdentityByIdDTO = { + id: string; +} & Omit; + export interface TIdentityTrustedIp { ipAddress: string; type: IPType; diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 02091d88c..a5514de71 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -178,7 +178,8 @@ export const integrationAuthServiceFactory = ({ actorAuthMethod, accessId, namespace, - accessToken + accessToken, + awsAssumeIamRoleArn }: TSaveIntegrationAccessTokenDTO) => { if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); @@ -230,7 +231,7 @@ export const integrationAuthServiceFactory = ({ updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt; } - if (!refreshToken && (accessId || accessToken)) { + if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) { if (accessToken) { const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, key); updateDoc.accessIV = accessEncToken.iv; @@ -243,6 +244,12 @@ export const integrationAuthServiceFactory = ({ updateDoc.accessIdTag = accessEncToken.tag; updateDoc.accessIdCiphertext = accessEncToken.ciphertext; } + if (awsAssumeIamRoleArn) { + const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, key); + updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext; + updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv; + updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag; + } } return integrationAuthDAL.create(updateDoc); }; @@ -251,6 +258,14 @@ export const integrationAuthServiceFactory = ({ const getIntegrationAccessToken = async (integrationAuth: TIntegrationAuths, botKey: string) => { let accessToken: string | undefined; let accessId: string | undefined; + // this means its not access token based + if ( + integrationAuth.integration === Integrations.AWS_SECRET_MANAGER && + integrationAuth.awsAssumeIamRoleArnCipherText + ) { + return { accessToken: "", accessId: "" }; + } + if (integrationAuth.accessTag && integrationAuth.accessIV && integrationAuth.accessCiphertext) { accessToken = decryptSymmetric128BitHexKeyUTF8({ ciphertext: integrationAuth.accessCiphertext, diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 0a816035c..5d1bfc18f 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -17,6 +17,7 @@ export type TSaveIntegrationAccessTokenDTO = { url?: string; namespace?: string; refreshToken?: string; + awsAssumeIamRoleArn?: string; } & TProjectPermission; export type TDeleteIntegrationAuthsDTO = TProjectPermission & { diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 70e3435a2..4d32f819a 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -17,14 +17,17 @@ import { UntagResourceCommand, UpdateSecretCommand } from "@aws-sdk/client-secrets-manager"; +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; import { Octokit } from "@octokit/rest"; import AWS, { AWSError } from "aws-sdk"; import { AxiosError } from "axios"; +import { randomUUID } from "crypto"; import sodium from "libsodium-wrappers"; import isEqual from "lodash.isequal"; import { z } from "zod"; import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -257,14 +260,27 @@ const syncSecretsGCPSecretManager = async ({ const syncSecretsAzureKeyVault = async ({ integration, secrets, - accessToken + accessToken, + createManySecretsRawFn, + updateManySecretsRawFn }: { - integration: TIntegrations; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; secrets: Record; accessToken: string; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; }) => { interface GetAzureKeyVaultSecret { id: string; // secret URI + value: string; attributes: { enabled: true; created: number; @@ -361,6 +377,83 @@ const syncSecretsAzureKeyVault = async ({ } }); + const secretsToAdd: { [key: string]: string } = {}; + const secretsToUpdate: { [key: string]: string } = {}; + const secretKeysToRemoveFromDelete = new Set(); + + const metadata = IntegrationMetadataSchema.parse(integration.metadata); + if (!integration.lastUsed) { + Object.keys(res).forEach((key) => { + // first time using integration + const underscoredKey = key.replace(/-/g, "_"); + + // -> apply initial sync behavior + switch (metadata.initialSyncBehavior) { + case IntegrationInitialSyncBehavior.PREFER_TARGET: { + if (!(underscoredKey in secrets)) { + secretsToAdd[underscoredKey] = res[key].value; + setSecrets.push({ + key, + value: res[key].value + }); + } else if (secrets[underscoredKey]?.value !== res[key].value) { + secretsToUpdate[underscoredKey] = res[key].value; + const toEditSecretIndex = setSecrets.findIndex((secret) => secret.key === key); + if (toEditSecretIndex >= 0) { + setSecrets[toEditSecretIndex].value = res[key].value; + } + } + + secretKeysToRemoveFromDelete.add(key); + + break; + } + case IntegrationInitialSyncBehavior.PREFER_SOURCE: { + if (!(underscoredKey in secrets)) { + secretsToAdd[underscoredKey] = res[key].value; + setSecrets.push({ + key, + value: res[key].value + }); + } + + secretKeysToRemoveFromDelete.add(key); + break; + } + default: + break; + } + }); + } + + 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: "" + })) + }); + } + + 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: "" + })) + }); + } + const setSecretAzureKeyVault = async ({ key, value, @@ -428,7 +521,7 @@ const syncSecretsAzureKeyVault = async ({ }); } - for await (const deleteSecret of deleteSecrets) { + for await (const deleteSecret of deleteSecrets.filter((secret) => !secretKeysToRemoveFromDelete.has(secret.key))) { const { key } = deleteSecret; await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { headers: { @@ -605,24 +698,61 @@ const syncSecretsAWSSecretManager = async ({ integration, secrets, accessId, - accessToken + accessToken, + awsAssumeRoleArn, + projectId }: { integration: TIntegrations; secrets: Record; accessId: string | null; accessToken: string; + awsAssumeRoleArn: string | null; + projectId?: string; }) => { + const appCfg = getConfig(); const metadata = z.record(z.any()).parse(integration.metadata || {}); - if (!accessId) { - throw new Error("AWS access ID is required"); + if (!accessId && !awsAssumeRoleArn) { + throw new Error("AWS access ID/AWS Assume Role is required"); + } + + let accessKeyId = ""; + let secretAccessKey = ""; + let sessionToken; + if (awsAssumeRoleArn) { + const client = new STSClient({ + region: integration.region as string, + credentials: + appCfg.CLIENT_ID_AWS_INTEGRATION && appCfg.CLIENT_SECRET_AWS_INTEGRATION + ? { + accessKeyId: appCfg.CLIENT_ID_AWS_INTEGRATION, + secretAccessKey: appCfg.CLIENT_SECRET_AWS_INTEGRATION + } + : undefined + }); + const command = new AssumeRoleCommand({ + RoleArn: awsAssumeRoleArn, + RoleSessionName: `infisical-sm-${randomUUID()}`, + DurationSeconds: 900, // 15mins + ExternalId: projectId + }); + const response = await client.send(command); + if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey) + throw new Error("Failed to assume role"); + accessKeyId = response.Credentials?.AccessKeyId; + secretAccessKey = response.Credentials?.SecretAccessKey; + sessionToken = response.Credentials?.SessionToken; + } else { + accessKeyId = accessId as string; + secretAccessKey = accessToken; } const secretsManager = new SecretsManagerClient({ region: integration.region as string, credentials: { - accessKeyId: accessId, - secretAccessKey: accessToken + accessKeyId, + secretAccessKey, + sessionToken } }); @@ -3478,7 +3608,9 @@ export const syncIntegrationSecrets = async ({ secrets, accessId, accessToken, - appendices + awsAssumeRoleArn, + appendices, + projectId }: { createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; @@ -3495,8 +3627,10 @@ export const syncIntegrationSecrets = async ({ integrationAuth: TIntegrationAuths; secrets: Record; accessId: string | null; + awsAssumeRoleArn: string | null; accessToken: string; appendices?: { prefix: string; suffix: string }; + projectId?: string; }) => { let response: { isSynced: boolean; syncMessage: string } | null = null; @@ -3512,7 +3646,9 @@ export const syncIntegrationSecrets = async ({ await syncSecretsAzureKeyVault({ integration, secrets, - accessToken + accessToken, + createManySecretsRawFn, + updateManySecretsRawFn }); break; case Integrations.AWS_PARAMETER_STORE: @@ -3528,7 +3664,9 @@ export const syncIntegrationSecrets = async ({ integration, secrets, accessId, - accessToken + accessToken, + awsAssumeRoleArn, + projectId }); break; case Integrations.HEROKU: diff --git a/backend/src/services/integration/integration-dal.ts b/backend/src/services/integration/integration-dal.ts index bada253c5..c98c7153a 100644 --- a/backend/src/services/integration/integration-dal.ts +++ b/backend/src/services/integration/integration-dal.ts @@ -22,7 +22,7 @@ export const integrationDALFactory = (db: TDbClient) => { const find = async (filter: Partial, tx?: Knex) => { try { - const docs = await integrationFindQuery(tx || db, filter); + const docs = await integrationFindQuery(tx || db.replicaNode(), filter); return docs.map(({ envId, envSlug, envName, ...el }) => ({ ...el, environment: { @@ -38,7 +38,7 @@ export const integrationDALFactory = (db: TDbClient) => { const findOne = async (filter: Partial, tx?: Knex) => { try { - const doc = await integrationFindQuery(tx || db, filter).first(); + const doc = await integrationFindQuery(tx || db.replicaNode(), filter).first(); if (!doc) return; const { envName: name, envSlug: slug, envId: id, ...el } = doc; @@ -50,7 +50,7 @@ export const integrationDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await integrationFindQuery(tx || db, { + const doc = await integrationFindQuery(tx || db.replicaNode(), { [`${TableName.Integration}.id` as "id"]: id }).first(); if (!doc) return; @@ -64,7 +64,7 @@ export const integrationDALFactory = (db: TDbClient) => { const findByProjectId = async (projectId: string, tx?: Knex) => { try { - const integrations = await (tx || db)(TableName.Integration) + const integrations = await (tx || db.replicaNode())(TableName.Integration) .where(`${TableName.Environment}.projectId`, projectId) .join(TableName.Environment, `${TableName.Integration}.envId`, `${TableName.Environment}.id`) .select(db.ref("name").withSchema(TableName.Environment).as("envName")) @@ -90,7 +90,7 @@ export const integrationDALFactory = (db: TDbClient) => { // used for syncing secrets // this will populate integration auth also const findByProjectIdV2 = async (projectId: string, environment: string, tx?: Knex) => { - const docs = await (tx || db)(TableName.Integration) + const docs = await (tx || db.replicaNode())(TableName.Integration) .where(`${TableName.Environment}.projectId`, projectId) .where("isActive", true) .where(`${TableName.Environment}.slug`, environment) @@ -120,7 +120,10 @@ export const integrationDALFactory = (db: TDbClient) => { db.ref("accessExpiresAt").withSchema(TableName.IntegrationAuth).as("accessExpiresAtAu"), db.ref("metadata").withSchema(TableName.IntegrationAuth).as("metadataAu"), db.ref("algorithm").withSchema(TableName.IntegrationAuth).as("algorithmAu"), - db.ref("keyEncoding").withSchema(TableName.IntegrationAuth).as("keyEncodingAu") + db.ref("keyEncoding").withSchema(TableName.IntegrationAuth).as("keyEncodingAu"), + db.ref("awsAssumeIamRoleArnCipherText").withSchema(TableName.IntegrationAuth), + db.ref("awsAssumeIamRoleArnIV").withSchema(TableName.IntegrationAuth), + db.ref("awsAssumeIamRoleArnTag").withSchema(TableName.IntegrationAuth) ); return docs.map( ({ @@ -146,6 +149,9 @@ export const integrationDALFactory = (db: TDbClient) => { algorithmAu: algorithm, keyEncodingAu: keyEncoding, accessExpiresAtAu: accessExpiresAt, + awsAssumeIamRoleArnIV, + awsAssumeIamRoleArnCipherText, + awsAssumeIamRoleArnTag, ...el }) => ({ ...el, @@ -174,7 +180,10 @@ export const integrationDALFactory = (db: TDbClient) => { metadata, algorithm, keyEncoding, - accessExpiresAt + accessExpiresAt, + awsAssumeIamRoleArnIV, + awsAssumeIamRoleArnCipherText, + awsAssumeIamRoleArnTag } }) ); diff --git a/backend/src/services/org/incident-contacts-dal.ts b/backend/src/services/org/incident-contacts-dal.ts index 1979a9c3e..9db87b517 100644 --- a/backend/src/services/org/incident-contacts-dal.ts +++ b/backend/src/services/org/incident-contacts-dal.ts @@ -16,7 +16,7 @@ export const incidentContactDALFactory = (db: TDbClient) => { const findByOrgId = async (orgId: string) => { try { - const incidentContacts = await db(TableName.IncidentContact).where({ orgId }); + const incidentContacts = await db.replicaNode()(TableName.IncidentContact).where({ orgId }); return incidentContacts; } catch (error) { throw new DatabaseError({ name: "Incident contact list", error }); @@ -25,7 +25,8 @@ export const incidentContactDALFactory = (db: TDbClient) => { const findOne = async (orgId: string, data: Partial) => { try { - const incidentContacts = await db(TableName.IncidentContact) + const incidentContacts = await db + .replicaNode()(TableName.IncidentContact) .where({ orgId, ...data }) .first(); return incidentContacts; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 1e52053b2..d518a698a 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -20,7 +20,7 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgById = async (orgId: string) => { try { - const org = await db(TableName.Organization).where({ id: orgId }).first(); + const org = await db.replicaNode()(TableName.Organization).where({ id: orgId }).first(); return org; } catch (error) { throw new DatabaseError({ error, name: "Find org by id" }); @@ -30,7 +30,8 @@ export const orgDALFactory = (db: TDbClient) => { // special query const findAllOrgsByUserId = async (userId: string): Promise => { try { - const org = await db(TableName.OrgMembership) + const org = await db + .replicaNode()(TableName.OrgMembership) .where({ userId }) .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) .select(selectAllTableCols(TableName.Organization)); @@ -42,7 +43,8 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgByProjectId = async (projectId: string): Promise => { try { - const [org] = await db(TableName.Project) + const [org] = await db + .replicaNode()(TableName.Project) .where({ [`${TableName.Project}.id` as "id"]: projectId }) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .select(selectAllTableCols(TableName.Organization)); @@ -56,7 +58,8 @@ export const orgDALFactory = (db: TDbClient) => { // special query const findAllOrgMembers = async (orgId: string) => { try { - const members = await db(TableName.OrgMembership) + const members = await db + .replicaNode()(TableName.OrgMembership) .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( @@ -95,7 +98,8 @@ export const orgDALFactory = (db: TDbClient) => { count: string; } - const count = await db(TableName.OrgMembership) + const count = await db + .replicaNode()(TableName.OrgMembership) .where(`${TableName.OrgMembership}.orgId`, orgId) .count("*") .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) @@ -110,7 +114,8 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => { try { - const members = await db(TableName.OrgMembership) + const members = await db + .replicaNode()(TableName.OrgMembership) .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( @@ -145,7 +150,8 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgGhostUser = async (orgId: string) => { try { - const member = await db(TableName.OrgMembership) + const member = await db + .replicaNode()(TableName.OrgMembership) .where({ orgId }) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) @@ -169,7 +175,8 @@ export const orgDALFactory = (db: TDbClient) => { const ghostUserExists = async (orgId: string) => { try { - const member = await db(TableName.OrgMembership) + const member = await db + .replicaNode()(TableName.OrgMembership) .where({ orgId }) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) @@ -257,7 +264,7 @@ export const orgDALFactory = (db: TDbClient) => { { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db)(TableName.OrgMembership) + const query = (tx || db.replicaNode())(TableName.OrgMembership) // eslint-disable-next-line .where(buildFindFilter(filter)) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 68d2b8cda..248bab568 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -420,13 +420,20 @@ export const orgServiceFactory = ({ } const plan = await licenseService.getPlan(orgId); - if (plan.memberLimit !== null && plan.membersUsed >= plan.memberLimit) { - // case: limit imposed on number of members allowed - // case: number of members used exceeds the number of members allowed + if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + // limit imposed on number of members allowed / number of members used exceeds the number of members allowed throw new BadRequestError({ message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." }); } + + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." + }); + } + const invitee = await orgDAL.transaction(async (tx) => { const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); if (inviteeUser) { diff --git a/backend/src/services/project-bot/project-bot-dal.ts b/backend/src/services/project-bot/project-bot-dal.ts index 74abf8f21..e25ebbd09 100644 --- a/backend/src/services/project-bot/project-bot-dal.ts +++ b/backend/src/services/project-bot/project-bot-dal.ts @@ -12,7 +12,7 @@ export const projectBotDALFactory = (db: TDbClient) => { const findOne = async (filter: Partial, tx?: Knex) => { try { - const bot = await (tx || db)(TableName.ProjectBot) + const bot = await (tx || db.replicaNode())(TableName.ProjectBot) .where(filter) .leftJoin(TableName.Users, `${TableName.ProjectBot}.senderId`, `${TableName.Users}.id`) .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) diff --git a/backend/src/services/project-env/project-env-dal.ts b/backend/src/services/project-env/project-env-dal.ts index 42a234298..d6f4429d0 100644 --- a/backend/src/services/project-env/project-env-dal.ts +++ b/backend/src/services/project-env/project-env-dal.ts @@ -12,7 +12,9 @@ export const projectEnvDALFactory = (db: TDbClient) => { const findBySlugs = async (projectId: string, env: string[], tx?: Knex) => { try { - const envs = await (tx || db)(TableName.Environment).where("projectId", projectId).whereIn("slug", env); + const envs = await (tx || db.replicaNode())(TableName.Environment) + .where("projectId", projectId) + .whereIn("slug", env); return envs; } catch (error) { throw new DatabaseError({ error, name: "Find by slugs" }); diff --git a/backend/src/services/project-key/project-key-dal.ts b/backend/src/services/project-key/project-key-dal.ts index d1b4053d0..ea4ed813c 100644 --- a/backend/src/services/project-key/project-key-dal.ts +++ b/backend/src/services/project-key/project-key-dal.ts @@ -16,7 +16,7 @@ export const projectKeyDALFactory = (db: TDbClient) => { tx?: Knex ): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => { try { - const projectKey = await (tx || db)(TableName.ProjectKeys) + const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys) .join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`) .join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) .where({ projectId, receiverId: userId }) @@ -34,7 +34,7 @@ export const projectKeyDALFactory = (db: TDbClient) => { const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => { try { - const pubKeys = await (tx || db)(TableName.ProjectMembership) + const pubKeys = await (tx || db.replicaNode())(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 590c26ecc..93ec6597e 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -13,7 +13,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => { // special query const findAllProjectMembers = async (projectId: string, filter: { usernames?: string[]; username?: string } = {}) => { try { - const docs = await db(TableName.ProjectMembership) + const docs = await db + .replicaNode()(TableName.ProjectMembership) .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .where((qb) => { @@ -108,7 +109,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectGhostUser = async (projectId: string, tx?: Knex) => { try { - const ghostUser = await (tx || db)(TableName.ProjectMembership) + const ghostUser = await (tx || db.replicaNode())(TableName.ProjectMembership) .where({ projectId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .select(selectAllTableCols(TableName.Users)) @@ -123,7 +124,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findMembershipsByUsername = async (projectId: string, usernames: string[]) => { try { - const members = await db(TableName.ProjectMembership) + const members = await db + .replicaNode()(TableName.ProjectMembership) .where({ projectId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join( @@ -149,7 +151,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectMembershipsByUserId = async (orgId: string, userId: string) => { try { - const memberships = await db(TableName.ProjectMembership) + const memberships = await db + .replicaNode()(TableName.ProjectMembership) .where({ userId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .where({ [`${TableName.Project}.orgId` as "orgId"]: orgId }) diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index a4ec99157..ce7f6324e 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -14,7 +14,8 @@ export const projectDALFactory = (db: TDbClient) => { const findAllProjects = async (userId: string) => { try { - const workspaces = await db(TableName.ProjectMembership) + const workspaces = await db + .replicaNode()(TableName.ProjectMembership) .where({ userId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) @@ -83,7 +84,7 @@ export const projectDALFactory = (db: TDbClient) => { const findProjectGhostUser = async (projectId: string, tx?: Knex) => { try { - const ghostUser = await (tx || db)(TableName.ProjectMembership) + const ghostUser = await (tx || db.replicaNode())(TableName.ProjectMembership) .where({ projectId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .select(selectAllTableCols(TableName.Users)) @@ -109,7 +110,8 @@ export const projectDALFactory = (db: TDbClient) => { const findAllProjectsByIdentity = async (identityId: string) => { try { - const workspaces = await db(TableName.IdentityProjectMembership) + const workspaces = await db + .replicaNode()(TableName.IdentityProjectMembership) .where({ identityId }) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) @@ -151,7 +153,8 @@ export const projectDALFactory = (db: TDbClient) => { const findProjectById = async (id: string) => { try { - const workspaces = await db(TableName.Project) + const workspaces = await db + .replicaNode()(TableName.Project) .where(`${TableName.Project}.id`, id) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( @@ -198,7 +201,8 @@ export const projectDALFactory = (db: TDbClient) => { throw new BadRequestError({ message: "Organization ID is required when querying with slugs" }); } - const projects = await db(TableName.Project) + const projects = await db + .replicaNode()(TableName.Project) .where(`${TableName.Project}.slug`, slug) .where(`${TableName.Project}.orgId`, orgId) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 1a8e65a41..981f90bb6 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -11,7 +11,7 @@ import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { createSecretBlindIndex } from "@app/lib/crypto"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; @@ -41,6 +41,7 @@ import { TListProjectCasDTO, TListProjectCertsDTO, TToggleProjectAutoCapitalizationDTO, + TUpdateAuditLogsRetentionDTO, TUpdateProjectDTO, TUpdateProjectNameDTO, TUpdateProjectVersionLimitDTO, @@ -446,6 +447,43 @@ export const projectServiceFactory = ({ return projectDAL.updateById(project.id, { pitVersionLimit }); }; + const updateAuditLogsRetention = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + auditLogsRetentionDays, + workspaceSlug + }: TUpdateAuditLogsRetentionDTO) => { + const project = await projectDAL.findProjectBySlug(workspaceSlug, actorOrgId); + if (!project) { + throw new NotFoundError({ + message: "Project not found." + }); + } + + const { hasRole } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new BadRequestError({ message: "Only admins are allowed to take this action" }); + } + + const plan = await licenseService.getPlan(project.orgId); + if (!plan.auditLogs || auditLogsRetentionDays > plan.auditLogsRetentionDays) { + throw new BadRequestError({ + message: "Failed to update audit logs retention due to plan limit reached. Upgrade plan to increase." + }); + } + + return projectDAL.updateById(project.id, { auditLogsRetentionDays }); + }; + const updateName = async ({ projectId, actor, @@ -621,6 +659,7 @@ export const projectServiceFactory = ({ upgradeProject, listProjectCas, listProjectCertificates, - updateVersionLimit + updateVersionLimit, + updateAuditLogsRetention }; }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index e2d145d3d..eb08fba98 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -49,6 +49,11 @@ export type TUpdateProjectVersionLimitDTO = { workspaceSlug: string; } & Omit; +export type TUpdateAuditLogsRetentionDTO = { + auditLogsRetentionDays: number; + workspaceSlug: string; +} & Omit; + export type TUpdateProjectNameDTO = { name: string; } & TProjectPermission; diff --git a/backend/src/services/secret-blind-index/secret-blind-index-dal.ts b/backend/src/services/secret-blind-index/secret-blind-index-dal.ts index 8fa60cde7..e26495ce3 100644 --- a/backend/src/services/secret-blind-index/secret-blind-index-dal.ts +++ b/backend/src/services/secret-blind-index/secret-blind-index-dal.ts @@ -12,7 +12,7 @@ export const secretBlindIndexDALFactory = (db: TDbClient) => { const countOfSecretsWithNullSecretBlindIndex = async (projectId: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.Secret) + const doc = await (tx || db.replicaNode())(TableName.Secret) .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.Secret}.folderId`) .leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`) .where({ projectId }) @@ -26,11 +26,10 @@ export const secretBlindIndexDALFactory = (db: TDbClient) => { const findAllSecretsByProjectId = async (projectId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.Secret) + const docs = await (tx || db.replicaNode())(TableName.Secret) .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.Secret}.folderId`) .leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`) .where({ projectId }) - .whereNull("secretBlindIndex") .select(selectAllTableCols(TableName.Secret)) .select( db.ref("slug").withSchema(TableName.Environment).as("environment"), @@ -44,12 +43,11 @@ export const secretBlindIndexDALFactory = (db: TDbClient) => { const findSecretsByProjectId = async (projectId: string, secretIds: string[], tx?: Knex) => { try { - const docs = await (tx || db)(TableName.Secret) + const docs = await (tx || db.replicaNode())(TableName.Secret) .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.Secret}.folderId`) .leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`) .where({ projectId }) .whereIn(`${TableName.Secret}.id`, secretIds) - .whereNull("secretBlindIndex") .select(selectAllTableCols(TableName.Secret)) .select( db.ref("slug").withSchema(TableName.Environment).as("environment"), diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 0e896d0c6..283f60c7c 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -211,7 +211,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { const findBySecretPath = async (projectId: string, environment: string, path: string, tx?: Knex) => { try { - const folder = await sqlFindFolderByPathQuery(tx || db, projectId, environment, removeTrailingSlash(path)) + const folder = await sqlFindFolderByPathQuery( + tx || db.replicaNode(), + projectId, + environment, + removeTrailingSlash(path) + ) .orderBy("depth", "desc") .first(); if (folder && folder.path !== removeTrailingSlash(path)) { @@ -230,7 +235,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { // it will stop automatically at /path2 const findClosestFolder = async (projectId: string, environment: string, path: string, tx?: Knex) => { try { - const folder = await sqlFindFolderByPathQuery(tx || db, projectId, environment, removeTrailingSlash(path)) + const folder = await sqlFindFolderByPathQuery( + tx || db.replicaNode(), + projectId, + environment, + removeTrailingSlash(path) + ) .orderBy("depth", "desc") .first(); if (!folder) return; @@ -247,7 +257,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { envId, secretPath: removeTrailingSlash(secretPath) })); - const folders = await sqlFindMultipleFolderByEnvPathQuery(tx || db, formatedQuery); + const folders = await sqlFindMultipleFolderByEnvPathQuery(tx || db.replicaNode(), formatedQuery); return formatedQuery.map(({ envId, secretPath }) => folders.find(({ path: targetPath, envId: targetEnvId }) => targetPath === secretPath && targetEnvId === envId) ); @@ -260,7 +270,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { // that is instances in which for a given folderid find the secret path const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => { try { - const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds); + const folders = await sqlFindSecretPathByFolderId(tx || db.replicaNode(), projectId, folderIds); // travelling all the way from leaf node to root contains real path const rootFolders = groupBy( @@ -299,7 +309,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const folder = await (tx || db)(TableName.SecretFolder) + const folder = await (tx || db.replicaNode())(TableName.SecretFolder) .where({ [`${TableName.SecretFolder}.id` as "id"]: id }) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .select(selectAllTableCols(TableName.SecretFolder)) diff --git a/backend/src/services/secret-folder/secret-folder-version-dal.ts b/backend/src/services/secret-folder/secret-folder-version-dal.ts index fb68ce801..ba0c21f20 100644 --- a/backend/src/services/secret-folder/secret-folder-version-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-version-dal.ts @@ -13,7 +13,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => { // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretFolderVersion) + const docs = await (tx || db.replicaNode())(TableName.SecretFolderVersion) .join(TableName.SecretFolder, `${TableName.SecretFolderVersion}.folderId`, `${TableName.SecretFolder}.id`) .where({ parentId: folderId, isReserved: false }) .join( @@ -38,7 +38,9 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => { const findLatestFolderVersions = async (folderIds: string[], tx?: Knex) => { try { - const docs: Array = await (tx || db)(TableName.SecretFolderVersion) + const docs: Array = await (tx || db.replicaNode())( + TableName.SecretFolderVersion + ) .whereIn("folderId", folderIds) .join( (tx || db)(TableName.SecretFolderVersion) diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index 0e73a8c23..9a7c7e4dc 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -51,7 +51,7 @@ export const secretImportDALFactory = (db: TDbClient) => { const find = async (filter: Partial, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretImport) + const docs = await (tx || db.replicaNode())(TableName.SecretImport) .where(filter) .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) .select( @@ -72,7 +72,7 @@ export const secretImportDALFactory = (db: TDbClient) => { const findByFolderIds = async (folderIds: string[], tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretImport) + const docs = await (tx || db.replicaNode())(TableName.SecretImport) .whereIn("folderId", folderIds) .where("isReplication", false) .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) diff --git a/backend/src/services/secret-tag/secret-tag-dal.ts b/backend/src/services/secret-tag/secret-tag-dal.ts index f1ae2424a..98cd9af22 100644 --- a/backend/src/services/secret-tag/secret-tag-dal.ts +++ b/backend/src/services/secret-tag/secret-tag-dal.ts @@ -13,7 +13,7 @@ export const secretTagDALFactory = (db: TDbClient) => { const findManyTagsById = async (projectId: string, ids: string[], tx?: Knex) => { try { - const tags = await (tx || db)(TableName.SecretTag).where({ projectId }).whereIn("id", ids); + const tags = await (tx || db.replicaNode())(TableName.SecretTag).where({ projectId }).whereIn("id", ids); return tags; } catch (error) { throw new DatabaseError({ error, name: "Find all by ids" }); diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index 790b403dd..c26880e38 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -114,7 +114,7 @@ export const secretDALFactory = (db: TDbClient) => { userId = undefined; } - const secs = await (tx || db)(TableName.Secret) + const secs = await (tx || db.replicaNode())(TableName.Secret) .where({ folderId }) .where((bd) => { void bd.whereNull("userId").orWhere({ userId: userId || null }); @@ -152,7 +152,7 @@ export const secretDALFactory = (db: TDbClient) => { const getSecretTags = async (secretId: string, tx?: Knex) => { try { - const tags = await (tx || db)(TableName.JnSecretTag) + const tags = await (tx || db.replicaNode())(TableName.JnSecretTag) .join(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) .where({ [`${TableName.Secret}Id` as const]: secretId }) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) @@ -179,7 +179,7 @@ export const secretDALFactory = (db: TDbClient) => { userId = undefined; } - const secs = await (tx || db)(TableName.Secret) + const secs = await (tx || db.replicaNode())(TableName.Secret) .whereIn("folderId", folderIds) .where((bd) => { void bd.whereNull("userId").orWhere({ userId: userId || null }); @@ -223,7 +223,7 @@ export const secretDALFactory = (db: TDbClient) => { ) => { if (!blindIndexes.length) return []; try { - const secrets = await (tx || db)(TableName.Secret) + const secrets = await (tx || db.replicaNode())(TableName.Secret) .where({ folderId }) .where((bd) => { blindIndexes.forEach((el) => { @@ -278,7 +278,7 @@ export const secretDALFactory = (db: TDbClient) => { const findReferencedSecretReferences = async (projectId: string, envSlug: string, secretPath: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretReference) + const docs = await (tx || db.replicaNode())(TableName.SecretReference) .where({ secretPath, environment: envSlug @@ -298,7 +298,7 @@ export const secretDALFactory = (db: TDbClient) => { // special query to backfill secret value const findAllProjectSecretValues = async (projectId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.Secret) + const docs = await (tx || db.replicaNode())(TableName.Secret) .join(TableName.SecretFolder, `${TableName.Secret}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .where("projectId", projectId) @@ -313,7 +313,7 @@ export const secretDALFactory = (db: TDbClient) => { const findOneWithTags = async (filter: Partial, tx?: Knex) => { try { - const rawDocs = await (tx || db)(TableName.Secret) + const rawDocs = await (tx || db.replicaNode())(TableName.Secret) .where(filter) .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index ac27d912f..afcb3a6bd 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -525,6 +525,18 @@ export const secretQueueFactory = ({ const botKey = await projectBotService.getBotKey(projectId); const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey); + const awsAssumeRoleArn = + integrationAuth.awsAssumeIamRoleArnTag && + integrationAuth.awsAssumeIamRoleArnIV && + integrationAuth.awsAssumeIamRoleArnCipherText + ? decryptSymmetric128BitHexKeyUTF8({ + ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText, + iv: integrationAuth.awsAssumeIamRoleArnIV, + tag: integrationAuth.awsAssumeIamRoleArnTag, + key: botKey + }) + : null; + const secrets = await getIntegrationSecrets({ environment, projectId, @@ -544,6 +556,8 @@ export const secretQueueFactory = ({ } try { + // akhilmhdh: this needs to changed later to be more easier to use + // at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places const response = await syncIntegrationSecrets({ createManySecretsRawFn, updateManySecretsRawFn, @@ -552,7 +566,9 @@ export const secretQueueFactory = ({ integrationAuth, secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, accessId: accessId as string, + awsAssumeRoleArn, accessToken, + projectId, appendices: { prefix: metadata?.secretPrefix || "", suffix: metadata?.secretSuffix || "" diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index a5a469a8f..cbfccba91 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1078,6 +1078,7 @@ export const secretServiceFactory = ({ actor, environment, projectId: workspaceId, + expandSecretReferences, projectSlug, actorId, actorOrgId, @@ -1091,7 +1092,7 @@ export const secretServiceFactory = ({ const botKey = await projectBotService.getBotKey(projectId); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); - const secret = await getSecretByName({ + const encryptedSecret = await getSecretByName({ actorId, projectId, actorAuthMethod, @@ -1105,7 +1106,46 @@ export const secretServiceFactory = ({ version }); - return decryptSecretRaw(secret, botKey); + const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); + + if (expandSecretReferences) { + const expandSecrets = interpolateSecrets({ + folderDAL, + projectId, + secretDAL, + secretEncKey: botKey + }); + + const expandSingleSecret = async (secret: { + secretKey: string; + secretValue: string; + secretComment?: string; + secretPath: string; + skipMultilineEncoding: boolean | null | undefined; + }) => { + const secretRecord: Record< + string, + { value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined } + > = { + [secret.secretKey]: { + value: secret.secretValue, + comment: secret.secretComment, + skipMultilineEncoding: secret.skipMultilineEncoding + } + }; + + await expandSecrets(secretRecord); + + // Update the secret with the expanded value + // eslint-disable-next-line no-param-reassign + secret.secretValue = secretRecord[secret.secretKey].value; + }; + + // Expand the secret + await expandSingleSecret(decryptedSecret); + } + + return decryptedSecret; }; const createSecretRaw = async ({ diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 1aac324c5..10df2f258 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -151,6 +151,7 @@ export type TGetASecretRawDTO = { secretName: string; path: string; environment: string; + expandSecretReferences?: boolean; type: "shared" | "personal"; includeImports?: boolean; version?: number; diff --git a/backend/src/services/secret/secret-version-dal.ts b/backend/src/services/secret/secret-version-dal.ts index 4d641bb8d..39a5089b2 100644 --- a/backend/src/services/secret/secret-version-dal.ts +++ b/backend/src/services/secret/secret-version-dal.ts @@ -13,7 +13,7 @@ export const secretVersionDALFactory = (db: TDbClient) => { // This will fetch all latest secret versions from a folder const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { try { - const docs = await (tx || db)(TableName.SecretVersion) + const docs = await (tx || db.replicaNode())(TableName.SecretVersion) .where(`${TableName.SecretVersion}.folderId`, folderId) .join(TableName.Secret, `${TableName.Secret}.id`, `${TableName.SecretVersion}.secretId`) .join( @@ -90,7 +90,7 @@ export const secretVersionDALFactory = (db: TDbClient) => { const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => { try { if (!secretIds.length) return {}; - const docs: Array = await (tx || db)(TableName.SecretVersion) + const docs: Array = await (tx || db.replicaNode())(TableName.SecretVersion) .where("folderId", folderId) .whereIn(`${TableName.SecretVersion}.secretId`, secretIds) .join( diff --git a/backend/src/services/service-token/service-token-dal.ts b/backend/src/services/service-token/service-token-dal.ts index 5d3fcc5c8..ed9c5de7e 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -12,7 +12,7 @@ export const serviceTokenDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await (tx || db)(TableName.ServiceToken) + const doc = await (tx || db.replicaNode())(TableName.ServiceToken) .leftJoin( TableName.Users, `${TableName.Users}.id`, diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index 64133ed7e..7e707e6fa 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -1,7 +1,57 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TSuperAdminDALFactory = ReturnType; -export const superAdminDALFactory = (db: TDbClient) => ormify(db, TableName.SuperAdmin, {}); +export const superAdminDALFactory = (db: TDbClient) => { + const superAdminOrm = ormify(db, TableName.SuperAdmin); + + const findById = async (id: string, tx?: Knex) => { + const config = await (tx || db)(TableName.SuperAdmin) + .where(`${TableName.SuperAdmin}.id`, id) + .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`) + .select( + db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin, + db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug") + ) + .first(); + + if (!config) { + return null; + } + + return { + ...config, + defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null + } as TSuperAdmin & { defaultAuthOrgSlug: string | null }; + }; + + const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => { + const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => { + await superAdminOrm.updateById(id, data, trx); + const config = await findById(id, trx); + + if (!config) { + throw new DatabaseError({ + error: "Failed to find updated super admin config", + message: "Failed to update super admin config", + name: "UpdateById" + }); + } + + return config; + }); + + return updatedConfig; + }; + + return { + ...superAdminOrm, + findById, + updateById + }; +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index f1d931b20..650910681 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -12,7 +12,7 @@ import { AuthMethod } from "../auth/auth-type"; import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { TAdminSignUpDTO } from "./super-admin-types"; +import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; @@ -25,7 +25,7 @@ type TSuperAdminServiceFactoryDep = { export type TSuperAdminServiceFactory = ReturnType; // eslint-disable-next-line -export let getServerCfg: () => Promise; +export let getServerCfg: () => Promise; const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s @@ -42,16 +42,20 @@ export const superAdminServiceFactory = ({ // TODO(akhilmhdh): bad pattern time less change this later to me itself 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 + + if (!serverCfg) { + throw new BadRequestError({ name: "Admin config", message: "Admin config not found" }); } + + await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore return serverCfg; } - const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin; + const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultAuthOrgSlug: string | null }; return { ...keyStoreServerCfg, // this is to allow admin router to work @@ -65,14 +69,51 @@ export const superAdminServiceFactory = ({ const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); if (serverCfg) return; - // @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 }); + const newCfg = await serverCfgDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: ADMIN_CONFIG_DB_UUID, + initialized: false, + allowSignUp: true, + defaultAuthOrgId: null + }); return newCfg; }; - const updateServerCfg = async (data: TSuperAdminUpdate) => { + const updateServerCfg = async (data: TSuperAdminUpdate, userId: string) => { + if (data.enabledLoginMethods) { + const superAdminUser = await userDAL.findById(userId); + const loginMethodToAuthMethod = { + [LoginMethod.EMAIL]: [AuthMethod.EMAIL], + [LoginMethod.GOOGLE]: [AuthMethod.GOOGLE], + [LoginMethod.GITLAB]: [AuthMethod.GITLAB], + [LoginMethod.GITHUB]: [AuthMethod.GITHUB], + [LoginMethod.LDAP]: [AuthMethod.LDAP], + [LoginMethod.OIDC]: [AuthMethod.OIDC], + [LoginMethod.SAML]: [ + AuthMethod.AZURE_SAML, + AuthMethod.GOOGLE_SAML, + AuthMethod.JUMPCLOUD_SAML, + AuthMethod.KEYCLOAK_SAML, + AuthMethod.OKTA_SAML + ] + }; + + if ( + !data.enabledLoginMethods.some((loginMethod) => + loginMethodToAuthMethod[loginMethod as LoginMethod].some( + (authMethod) => superAdminUser.authMethods?.includes(authMethod) + ) + ) + ) { + throw new BadRequestError({ + message: "You must configure at least one auth method to prevent account lockout" + }); + } + } 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; }; @@ -98,6 +139,7 @@ export const superAdminServiceFactory = ({ if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" }); const privateKey = await getUserPrivateKey(password, { + encryptionVersion: 2, salt, protectedKey, protectedKeyIV, @@ -155,7 +197,7 @@ export const superAdminServiceFactory = ({ orgName: initialOrganizationName }); - await updateServerCfg({ initialized: true }); + await updateServerCfg({ initialized: true }, userInfo.user.id); const token = await authService.generateUserTokens({ user: userInfo.user, authMethod: AuthMethod.EMAIL, @@ -167,9 +209,25 @@ export const superAdminServiceFactory = ({ return { token, user: userInfo, organization }; }; + const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => { + return userDAL.getUsersByFilter({ + limit, + offset, + searchTerm, + sortBy: "username" + }); + }; + + const deleteUser = async (userId: string) => { + const user = await userDAL.deleteById(userId); + return user; + }; + return { initServerCfg, updateServerCfg, - adminSignUp + adminSignUp, + getUsers, + deleteUser }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index e444c8843..2d10941b4 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -15,3 +15,19 @@ export type TAdminSignUpDTO = { ip: string; userAgent: string; }; + +export type TAdminGetUsersDTO = { + offset: number; + limit: number; + searchTerm: string; +}; + +export enum LoginMethod { + EMAIL = "email", + GOOGLE = "google", + GITHUB = "github", + GITLAB = "gitlab", + SAML = "saml", + LDAP = "ldap", + OIDC = "oidc" +} diff --git a/backend/src/services/user-alias/user-alias-types.ts b/backend/src/services/user-alias/user-alias-types.ts index 09204644f..7207e8acf 100644 --- a/backend/src/services/user-alias/user-alias-types.ts +++ b/backend/src/services/user-alias/user-alias-types.ts @@ -1,4 +1,5 @@ export enum UserAliasType { LDAP = "ldap", - SAML = "saml" + SAML = "saml", + OIDC = "oidc" } diff --git a/backend/src/services/user-engagement/user-engagement-service.ts b/backend/src/services/user-engagement/user-engagement-service.ts new file mode 100644 index 000000000..5d7b54929 --- /dev/null +++ b/backend/src/services/user-engagement/user-engagement-service.ts @@ -0,0 +1,89 @@ +import { PlainClient } from "@team-plain/typescript-sdk"; + +import { getConfig } from "@app/lib/config/env"; +import { InternalServerError } from "@app/lib/errors"; + +import { TUserDALFactory } from "../user/user-dal"; + +type TUserEngagementServiceFactoryDep = { + userDAL: Pick; +}; + +export type TUserEngagementServiceFactory = ReturnType; + +export const userEngagementServiceFactory = ({ userDAL }: TUserEngagementServiceFactoryDep) => { + const createUserWish = async (userId: string, text: string) => { + const user = await userDAL.findById(userId); + const appCfg = getConfig(); + + if (!appCfg.PLAIN_API_KEY) { + throw new InternalServerError({ + message: "Plain is not configured." + }); + } + + const client = new PlainClient({ + apiKey: appCfg.PLAIN_API_KEY + }); + + const customerUpsertRes = await client.upsertCustomer({ + identifier: { + emailAddress: user.email + }, + onCreate: { + fullName: `${user.firstName} ${user.lastName}`, + shortName: user.firstName, + email: { + email: user.email as string, + isVerified: user.isEmailVerified as boolean + }, + + externalId: user.id + }, + + onUpdate: { + fullName: { + value: `${user.firstName} ${user.lastName}` + }, + shortName: { + value: user.firstName + }, + email: { + email: user.email as string, + isVerified: user.isEmailVerified as boolean + }, + externalId: { + value: user.id + } + } + }); + + if (customerUpsertRes.error) { + throw new InternalServerError({ message: customerUpsertRes.error.message }); + } + + const createThreadRes = await client.createThread({ + title: "Wish", + customerIdentifier: { + externalId: customerUpsertRes.data.customer.externalId + }, + components: [ + { + componentText: { + text + } + } + ], + labelTypeIds: appCfg.PLAIN_WISH_LABEL_IDS?.split(",") + }); + + if (createThreadRes.error) { + throw new InternalServerError({ + message: createThreadRes.error.message + }); + } + }; + return { + createUserWish + }; +}; diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index f2da0df0e..9ec495ca3 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -7,10 +7,11 @@ import { TUserActionsUpdate, TUserEncryptionKeys, TUserEncryptionKeysInsert, - TUserEncryptionKeysUpdate + TUserEncryptionKeysUpdate, + TUsers } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TUserDALFactory = ReturnType; @@ -18,11 +19,45 @@ export const userDALFactory = (db: TDbClient) => { const userOrm = ormify(db, TableName.Users); const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx); + const getUsersByFilter = async ({ + limit, + offset, + searchTerm, + sortBy + }: { + limit: number; + offset: number; + searchTerm: string; + sortBy?: keyof TUsers; + }) => { + try { + let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false); + if (searchTerm) { + query = query.where((qb) => { + void qb + .whereILike("email", `%${searchTerm}%`) + .orWhereILike("firstName", `%${searchTerm}%`) + .orWhereILike("lastName", `%${searchTerm}%`) + .orWhereLike("username", `%${searchTerm}%`); + }); + } + + if (sortBy) { + query = query.orderBy(sortBy); + } + + return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users)); + } catch (error) { + throw new DatabaseError({ error, name: "Get users by filter" }); + } + }; + // USER ENCRYPTION FUNCTIONS // ------------------------- const findUserEncKeyByUsername = async ({ username }: { username: string }) => { try { - return await db(TableName.Users) + return await db + .replicaNode()(TableName.Users) .where({ username, isGhost: false @@ -36,7 +71,7 @@ export const userDALFactory = (db: TDbClient) => { const findUserEncKeyByUserIdsBatch = async ({ userIds }: { userIds: string[] }, tx?: Knex) => { try { - return await (tx || db)(TableName.Users) + return await (tx || db.replicaNode())(TableName.Users) .where({ isGhost: false }) @@ -49,7 +84,8 @@ export const userDALFactory = (db: TDbClient) => { const findUserEncKeyByUserId = async (userId: string) => { try { - const user = await db(TableName.Users) + const user = await db + .replicaNode()(TableName.Users) .where(`${TableName.Users}.id`, userId) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .first(); @@ -65,7 +101,8 @@ export const userDALFactory = (db: TDbClient) => { const findUserByProjectMembershipId = async (projectMembershipId: string) => { try { - return await db(TableName.ProjectMembership) + return await db + .replicaNode()(TableName.ProjectMembership) .where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .first(); @@ -76,7 +113,8 @@ export const userDALFactory = (db: TDbClient) => { const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => { try { - return await db(TableName.ProjectMembership) + return await db + .replicaNode()(TableName.ProjectMembership) .whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .select("*"); @@ -128,7 +166,7 @@ export const userDALFactory = (db: TDbClient) => { // --------------------- const findOneUserAction = (filter: TUserActionsUpdate, tx?: Knex) => { try { - return (tx || db)(TableName.UserAction).where(filter).first("*"); + return (tx || db.replicaNode())(TableName.UserAction).where(filter).first("*"); } catch (error) { throw new DatabaseError({ error, name: "Find one user action" }); } @@ -155,6 +193,7 @@ export const userDALFactory = (db: TDbClient) => { upsertUserEncryptionKey, createUserEncryption, findOneUserAction, - createUserAction + createUserAction, + getUsersByFilter }; }; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 4ee8bdc1f..f0b043279 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -8,6 +8,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { AuthMethod } from "../auth/auth-type"; +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TUserDALFactory } from "./user-dal"; type TUserServiceFactoryDep = { @@ -26,8 +27,9 @@ type TUserServiceFactoryDep = { | "delete" >; userAliasDAL: Pick; - orgMembershipDAL: Pick; + orgMembershipDAL: Pick; tokenService: Pick; + projectMembershipDAL: Pick; smtpService: Pick; }; @@ -37,6 +39,7 @@ export const userServiceFactory = ({ userDAL, userAliasDAL, orgMembershipDAL, + projectMembershipDAL, tokenService, smtpService }: TUserServiceFactoryDep) => { @@ -198,7 +201,7 @@ export const userServiceFactory = ({ return user; }; - const deleteMe = async (userId: string) => { + const deleteUser = async (userId: string) => { const user = await userDAL.deleteById(userId); return user; }; @@ -247,17 +250,64 @@ export const userServiceFactory = ({ return privateKey; }; + const getUserProjectFavorites = async (userId: string, orgId: string) => { + const orgMembership = await orgMembershipDAL.findOne({ + userId, + orgId + }); + + if (!orgMembership) { + throw new BadRequestError({ + message: "User does not belong in the organization." + }); + } + + return { projectFavorites: orgMembership.projectFavorites || [] }; + }; + + const updateUserProjectFavorites = async (userId: string, orgId: string, projectIds: string[]) => { + const orgMembership = await orgMembershipDAL.findOne({ + userId, + orgId + }); + + if (!orgMembership) { + throw new BadRequestError({ + message: "User does not belong in the organization." + }); + } + + const matchingUserProjectMemberships = await projectMembershipDAL.find({ + userId, + $in: { + projectId: projectIds + } + }); + + const memberProjectFavorites = matchingUserProjectMemberships.map( + (projectMembership) => projectMembership.projectId + ); + + const updatedOrgMembership = await orgMembershipDAL.updateById(orgMembership.id, { + projectFavorites: memberProjectFavorites + }); + + return updatedOrgMembership.projectFavorites; + }; + return { sendEmailVerificationCode, verifyEmailVerificationCode, toggleUserMfa, updateUserName, updateAuthMethods, - deleteMe, + deleteUser, getMe, createUserAction, getUserAction, unlockUser, - getUserPrivateKey + getUserPrivateKey, + getUserProjectFavorites, + updateUserProjectFavorites }; }; diff --git a/backend/src/services/webhook/webhook-dal.ts b/backend/src/services/webhook/webhook-dal.ts index c33d79fdb..14d30a35e 100644 --- a/backend/src/services/webhook/webhook-dal.ts +++ b/backend/src/services/webhook/webhook-dal.ts @@ -22,7 +22,7 @@ export const webhookDALFactory = (db: TDbClient) => { const find = async (filter: Partial, tx?: Knex) => { try { - const docs = await webhookFindQuery(tx || db, filter); + const docs = await webhookFindQuery(tx || db.replicaNode(), filter); return docs.map(({ envId, envSlug, envName, ...el }) => ({ ...el, envId, @@ -39,7 +39,7 @@ export const webhookDALFactory = (db: TDbClient) => { const findOne = async (filter: Partial, tx?: Knex) => { try { - const doc = await webhookFindQuery(tx || db, filter).first(); + const doc = await webhookFindQuery(tx || db.replicaNode(), filter).first(); if (!doc) return; const { envName: name, envSlug: slug, envId: id, ...el } = doc; @@ -51,7 +51,7 @@ export const webhookDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await webhookFindQuery(tx || db, { + const doc = await webhookFindQuery(tx || db.replicaNode(), { [`${TableName.Webhook}.id` as "id"]: id }).first(); if (!doc) return; @@ -65,7 +65,7 @@ export const webhookDALFactory = (db: TDbClient) => { const findAllWebhooks = async (projectId: string, environment?: string, secretPath?: string, tx?: Knex) => { try { - const webhooks = await (tx || db)(TableName.Webhook) + const webhooks = await (tx || db.replicaNode())(TableName.Webhook) .where(`${TableName.Environment}.projectId`, projectId) .where((qb) => { if (environment) { diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 35d2ba7fc..2439c7d65 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -4,55 +4,63 @@ import { AxiosError } from "axios"; import picomatch from "picomatch"; import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; -import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { decryptSymmetric, decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; +import { WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -export const triggerWebhookRequest = async ( - { url, encryptedSecretKey, iv, tag, keyEncoding }: TWebhooks, - data: Record -) => { - const headers: Record = {}; - const payload = { ...data, timestamp: Date.now() }; - const appCfg = getConfig(); + +export const decryptWebhookDetails = (webhook: TWebhooks) => { + const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; + + let decryptedSecretKey = ""; + let decryptedUrl = url; if (encryptedSecretKey) { - const encryptionKey = appCfg.ENCRYPTION_KEY; - const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; - let secretKey; - if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) { - // case: encoding scheme is base64 - secretKey = decryptSymmetric({ - ciphertext: encryptedSecretKey, - iv: iv as string, - tag: tag as string, - key: rootEncryptionKey - }); - } else if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) { - // case: encoding scheme is utf8 - secretKey = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: encryptedSecretKey, - iv: iv as string, - tag: tag as string, - key: encryptionKey - }); - } - if (secretKey) { - const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); - headers["x-infisical-signature"] = `t=${payload.timestamp};${webhookSign}`; - } + decryptedSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: keyEncoding as SecretKeyEncoding, + ciphertext: encryptedSecretKey, + iv: iv as string, + tag: tag as string + }); } + + if (urlCipherText) { + decryptedUrl = infisicalSymmetricDecrypt({ + keyEncoding: keyEncoding as SecretKeyEncoding, + ciphertext: urlCipherText, + iv: urlIV as string, + tag: urlTag as string + }); + } + + return { + secretKey: decryptedSecretKey, + url: decryptedUrl + }; +}; + +export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record) => { + const headers: Record = {}; + const payload = { ...data, timestamp: Date.now() }; + const { secretKey, url } = decryptWebhookDetails(webhook); + + if (secretKey) { + const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); + headers["x-infisical-signature"] = `t=${payload.timestamp};${webhookSign}`; + } + const req = await request.post(url, payload, { headers, timeout: WEBHOOK_TRIGGER_TIMEOUT, signal: AbortSignal.timeout(WEBHOOK_TRIGGER_TIMEOUT) }); + return req; }; @@ -60,15 +68,48 @@ export const getWebhookPayload = ( eventName: string, workspaceId: string, environment: string, - secretPath?: string -) => ({ - event: eventName, - project: { - workspaceId, - environment, - secretPath + secretPath?: string, + type?: string | null +) => { + switch (type) { + case WebhookType.SLACK: + return { + text: "A secret value has been added or modified.", + attachments: [ + { + color: "#E7F256", + fields: [ + { + title: "Workspace ID", + value: workspaceId, + short: false + }, + { + title: "Environment", + value: environment, + short: false + }, + { + title: "Secret Path", + value: secretPath, + short: false + } + ] + } + ] + }; + case WebhookType.GENERAL: + default: + return { + event: eventName, + project: { + workspaceId, + environment, + secretPath + } + }; } -}); +}; export type TFnTriggerWebhookDTO = { projectId: string; @@ -95,9 +136,10 @@ export const fnTriggerWebhook = async ({ logger.info("Secret webhook job started", { environment, secretPath, projectId }); const webhooksTriggered = await Promise.allSettled( toBeTriggeredHooks.map((hook) => - triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath)) + triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath, hook.type)) ) ); + // filter hooks by status const successWebhooks = webhooksTriggered .filter(({ status }) => status === "fulfilled") diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index 4a05ad219..272c9de90 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,15 +1,14 @@ import { ForbiddenError } from "@casl/ability"; -import { SecretEncryptionAlgo, SecretKeyEncoding, TWebhooksInsert } from "@app/db/schemas"; +import { TWebhooksInsert } 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 { encryptSymmetric, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; -import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; +import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; import { TCreateWebhookDTO, TDeleteWebhookDTO, @@ -36,7 +35,8 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer webhookUrl, environment, secretPath, - webhookSecretKey + webhookSecretKey, + type }: TCreateWebhookDTO) => { const { permission } = await permissionService.getProjectPermission( actor, @@ -50,30 +50,29 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer if (!env) throw new BadRequestError({ message: "Env not found" }); const insertDoc: TWebhooksInsert = { - url: webhookUrl, + url: "", // deprecated - we are moving away from plaintext URLs envId: env.id, isDisabled: false, - secretPath: secretPath || "/" + secretPath: secretPath || "/", + type }; + if (webhookSecretKey) { - const appCfg = getConfig(); - const encryptionKey = appCfg.ENCRYPTION_KEY; - const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; - if (rootEncryptionKey) { - const { ciphertext, iv, tag } = encryptSymmetric(webhookSecretKey, rootEncryptionKey); - insertDoc.encryptedSecretKey = ciphertext; - insertDoc.iv = iv; - insertDoc.tag = tag; - insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM; - insertDoc.keyEncoding = SecretKeyEncoding.BASE64; - } else if (encryptionKey) { - const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(webhookSecretKey, encryptionKey); - insertDoc.encryptedSecretKey = ciphertext; - insertDoc.iv = iv; - insertDoc.tag = tag; - insertDoc.algorithm = SecretEncryptionAlgo.AES_256_GCM; - insertDoc.keyEncoding = SecretKeyEncoding.UTF8; - } + const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey); + insertDoc.encryptedSecretKey = ciphertext; + insertDoc.iv = iv; + insertDoc.tag = tag; + insertDoc.algorithm = algorithm; + insertDoc.keyEncoding = encoding; + } + + if (webhookUrl) { + const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl); + insertDoc.urlCipherText = ciphertext; + insertDoc.urlIV = iv; + insertDoc.urlTag = tag; + insertDoc.algorithm = algorithm; + insertDoc.keyEncoding = encoding; } const webhook = await webhookDAL.create(insertDoc); @@ -131,7 +130,7 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer try { await triggerWebhookRequest( webhook, - getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath) + getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath, webhook.type) ); } catch (err) { webhookError = (err as Error).message; @@ -162,7 +161,14 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); - return webhookDAL.findAllWebhooks(projectId, environment, secretPath); + const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); + return webhooks.map((w) => { + const { url } = decryptWebhookDetails(w); + return { + ...w, + url + }; + }); }; return { diff --git a/backend/src/services/webhook/webhook-types.ts b/backend/src/services/webhook/webhook-types.ts index 7a6e92c80..40dacb42a 100644 --- a/backend/src/services/webhook/webhook-types.ts +++ b/backend/src/services/webhook/webhook-types.ts @@ -5,6 +5,7 @@ export type TCreateWebhookDTO = { secretPath?: string; webhookUrl: string; webhookSecretKey?: string; + type: string; } & TProjectPermission; export type TUpdateWebhookDTO = { @@ -24,3 +25,8 @@ export type TListWebhookDTO = { environment?: string; secretPath?: string; } & TProjectPermission; + +export enum WebhookType { + GENERAL = "general", + SLACK = "slack" +} diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 43c159c34..a56e002dc 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -73,6 +73,11 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + plainOutput, err := cmd.Flags().GetBool("plain") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + request := models.GetAllSecretsParameters{ Environment: environmentName, WorkspaceId: projectId, @@ -100,7 +105,6 @@ var secretsCmd = &cobra.Command{ } if shouldExpandSecrets { - authParams := models.ExpandSecretsAuthentication{} if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { authParams.InfisicalToken = token.Token @@ -114,7 +118,14 @@ var secretsCmd = &cobra.Command{ // Sort the secrets by key so we can create a consistent output secrets = util.SortSecretsByKeys(secrets) - visualize.PrintAllSecretDetails(secrets) + if plainOutput { + for _, secret := range secrets { + fmt.Println(secret.Value) + } + } else { + visualize.PrintAllSecretDetails(secrets) + } + Telemetry.CaptureEvent("cli-command:secrets", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) }, } @@ -325,9 +336,20 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse recursive flag") } + // deprecated, in favor of --plain showOnlyValue, err := cmd.Flags().GetBool("raw-value") if err != nil { - util.HandleError(err, "Unable to parse path flag") + util.HandleError(err, "Unable to parse flag") + } + + plainOutput, err := cmd.Flags().GetBool("plain") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + includeImports, err := cmd.Flags().GetBool("include-imports") + if err != nil { + util.HandleError(err, "Unable to parse flag") } request := models.GetAllSecretsParameters{ @@ -335,7 +357,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { WorkspaceId: projectId, TagSlugs: tagSlugs, SecretsPath: secretsPath, - IncludeImport: true, + IncludeImport: includeImports, Recursive: recursive, } @@ -377,15 +399,15 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } } - if showOnlyValue && len(requestedSecrets) > 1 { - util.PrintErrorMessageAndExit("--raw-value only works with one secret.") - } - - if showOnlyValue { - fmt.Printf(requestedSecrets[0].Value) + // showOnlyValue deprecated in favor of --plain, below only for backward compatibility + if plainOutput || showOnlyValue { + for _, secret := range requestedSecrets { + fmt.Println(secret.Value) + } } else { visualize.PrintAllSecretDetails(requestedSecrets) } + Telemetry.CaptureEvent("cli-command:secrets get", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) } @@ -639,11 +661,12 @@ func init() { secretsGetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") secretsGetCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") - secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") - secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") + secretsGetCmd.Flags().Bool("plain", false, "print values without formatting, one per line") + secretsGetCmd.Flags().Bool("raw-value", false, "deprecated. Returns only the value of secret, only works with one secret. Use --plain instead") + secretsGetCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") + secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets") secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") secretsCmd.AddCommand(secretsGetCmd) - secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) secretsSetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") @@ -687,10 +710,11 @@ func init() { secretsCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets when using machine identity based auth") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") - secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") + secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets") secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") secretsCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.Flags().String("path", "/", "get secrets within a folder path") + secretsCmd.Flags().Bool("plain", false, "print values without formatting, one per line") rootCmd.AddCommand(secretsCmd) } diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 08e6b563f..dcb431cca 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -885,7 +885,7 @@ func SetEncryptedSecrets(secretArgs []string, secretType string, environmentName } // Key and value from argument - key := splitKeyValueFromArg[0] + key := strings.TrimSpace(splitKeyValueFromArg[0]) value := splitKeyValueFromArg[1] hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) diff --git a/company/handbook/time-off.mdx b/company/handbook/time-off.mdx index a80721440..aae111878 100644 --- a/company/handbook/time-off.mdx +++ b/company/handbook/time-off.mdx @@ -10,4 +10,8 @@ To request time off, just submit a request in Rippling and let Maidul know at le ## National holidays -Since Infisical's team is globally distributed, it is hard for us to keep track of all the various national holidays across many different countries. Whether you'd like to celebrate Christmas or National Brisket Day (which, by the way, is on May 28th), you are welcome to take PTO on those days – just let Maidul know at least a week ahead so that we can adjust our planning. \ No newline at end of file +Since Infisical's team is globally distributed, it is hard for us to keep track of all the various national holidays across many different countries. Whether you'd like to celebrate Christmas or National Brisket Day (which, by the way, is on May 28th), you are welcome to take PTO on those days – just let Maidul know at least a week ahead so that we can adjust our planning. + +## Winter Break + +Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla). \ No newline at end of file diff --git a/company/mint.json b/company/mint.json index 0ae63107b..ea9c41e21 100644 --- a/company/mint.json +++ b/company/mint.json @@ -64,5 +64,10 @@ ], "integrations": { "intercom": "hsg644ru" + }, + "analytics": { + "koala": { + "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d" + } } } diff --git a/company/style.css b/company/style.css index ea8c60dc9..f5e1bfc49 100644 --- a/company/style.css +++ b/company/style.css @@ -10,7 +10,6 @@ #sidebar { left: 0; - padding-left: 48px; padding-right: 30px; border-right: 1px; border-color: #cdd64b; @@ -18,6 +17,10 @@ border-right: 1px solid #ebebeb; } +#sidebar-content { + padding-left: 2rem; +} + #sidebar .relative .sticky { opacity: 0; } diff --git a/docker-compose.dev-read-replica.yml b/docker-compose.dev-read-replica.yml new file mode 100644 index 000000000..7d1e6e7fe --- /dev/null +++ b/docker-compose.dev-read-replica.yml @@ -0,0 +1,191 @@ +version: "3.9" + +services: + nginx: + container_name: infisical-dev-nginx + image: nginx + restart: always + ports: + - 8080:80 + volumes: + - ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - backend + - frontend + + db: + image: bitnami/postgresql:14 + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRESQL_PASSWORD: infisical + POSTGRESQL_USERNAME: infisical + POSTGRESQL_DATABASE: infisical + POSTGRESQL_REPLICATION_MODE: master + POSTGRESQL_REPLICATION_USER: repl_user + POSTGRESQL_REPLICATION_PASSWORD: repl_password + POSTGRESQL_SYNCHRONOUS_COMMIT_MODE: on + POSTGRESQL_NUM_SYNCHRONOUS_REPLICAS: 1 + + db-slave: + image: bitnami/postgresql:14 + ports: + - "5433:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRESQL_PASSWORD: infisical + POSTGRESQL_USERNAME: infisical + POSTGRESQL_DATABASE: infisical + POSTGRESQL_REPLICATION_MODE: slave + POSTGRESQL_REPLICATION_USER: repl_user + POSTGRESQL_REPLICATION_PASSWORD: repl_password + POSTGRESQL_MASTER_HOST: db + POSTGRESQL_MASTER_PORT_NUMBER: 5432 + + + redis: + image: redis + container_name: infisical-dev-redis + environment: + - ALLOW_EMPTY_PASSWORD=yes + ports: + - 6379:6379 + volumes: + - redis_data:/data + + redis-commander: + container_name: infisical-dev-redis-commander + image: rediscommander/redis-commander + restart: always + depends_on: + - redis + environment: + - REDIS_HOSTS=local:redis:6379 + ports: + - "8085:8081" + + db-test: + profiles: ["test"] + image: postgres:14-alpine + ports: + - "5430:5432" + environment: + POSTGRES_PASSWORD: infisical + POSTGRES_USER: infisical + POSTGRES_DB: infisical-test + + db-migration: + container_name: infisical-db-migration + depends_on: + - db + build: + context: ./backend + dockerfile: Dockerfile.dev + env_file: .env + environment: + - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable + command: npm run migration:latest + volumes: + - ./backend/src:/app/src + + backend: + container_name: infisical-dev-api + build: + context: ./backend + dockerfile: Dockerfile.dev + depends_on: + db: + condition: service_started + redis: + condition: service_started + db-migration: + condition: service_completed_successfully + env_file: + - .env + ports: + - 4000:4000 + environment: + - NODE_ENV=development + - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable + - TELEMETRY_ENABLED=false + volumes: + - ./backend/src:/app/src + extra_hosts: + - "host.docker.internal:host-gateway" + + frontend: + container_name: infisical-dev-frontend + restart: unless-stopped + depends_on: + - backend + build: + context: ./frontend + dockerfile: Dockerfile.dev + volumes: + - ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files + - ./frontend/public:/app/public + env_file: .env + environment: + - NEXT_PUBLIC_ENV=development + - INFISICAL_TELEMETRY_ENABLED=false + + pgadmin: + image: dpage/pgadmin4 + restart: always + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: pass + ports: + - 5050:80 + depends_on: + - db + + smtp-server: + container_name: infisical-dev-smtp-server + image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362 + restart: always + logging: + driver: "none" # disable saving logs + 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 + postgres-slave-data: + driver: local + redis_data: + driver: local + ldap_data: + ldap_config: diff --git a/docs/api-reference/endpoints/identities/get-by-id.mdx b/docs/api-reference/endpoints/identities/get-by-id.mdx new file mode 100644 index 000000000..f721d3556 --- /dev/null +++ b/docs/api-reference/endpoints/identities/get-by-id.mdx @@ -0,0 +1,5 @@ +--- +title: "Get By ID" +openapi: "GET /api/v1/identities/{identityId}" +--- + diff --git a/docs/api-reference/endpoints/identities/list.mdx b/docs/api-reference/endpoints/identities/list.mdx new file mode 100644 index 000000000..d8972e3a9 --- /dev/null +++ b/docs/api-reference/endpoints/identities/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/identities" +--- diff --git a/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx b/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx new file mode 100644 index 000000000..477ee875c --- /dev/null +++ b/docs/api-reference/endpoints/universal-auth/get-client-secret-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Client Secret By ID" +openapi: "GET /api/v1/auth/universal-auth/identities/{identityId}/client-secrets/{clientSecretId}" +--- diff --git a/docs/api-reference/endpoints/universal-auth/revoke.mdx b/docs/api-reference/endpoints/universal-auth/revoke.mdx new file mode 100644 index 000000000..e2a19e93c --- /dev/null +++ b/docs/api-reference/endpoints/universal-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/universal-auth/identities/{identityId}" +--- diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index c279b1a15..03ebde1c0 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -88,6 +88,27 @@ $ infisical secrets ``` + + The `--plain` flag will output all your secret values without formatting, one per line. + + ```bash + # Example + infisical secrets --plain --silent + ``` + + + + + The `--silent` flag disables output of tip/info messages. Useful when running in scripts or CI/CD pipelines. + + ```bash + # Example + infisical secrets --silent + ``` + + Can be used inline to replace `INFISICAL_DISABLE_UPDATE_CHECK` + + @@ -99,6 +120,7 @@ $ infisical secrets get ... # Example $ infisical secrets get DOMAIN +$ infisical secrets get DOMAIN PORT ``` @@ -111,7 +133,41 @@ $ infisical secrets get DOMAIN - + + The `--plain` flag will output all your requested secret values without formatting, one per line. + + Default value: `false` + + ```bash + # Example + infisical secrets get FOO --plain + infisical secrets get FOO BAR --plain + + # Fetch a single value and assign it to a variable + API_KEY=$(infisical secrets get FOO --plain --silent) + ``` + + + When running in CI/CD environments or in a script, set `INFISICAL_DISABLE_UPDATE_CHECK=true` or add the `--silent` flag. This will help hide any CLI info/debug output and only show the secret value. + + + + + + The `--silent` flag disables output of tip/info messages. Useful when running in scripts or CI/CD pipelines. + + ```bash + # Example + infisical secrets get FOO --plain --silent + ``` + + Can be used inline to replace `INFISICAL_DISABLE_UPDATE_CHECK` + + + + + Use `--plain` instead, as it supports single and multiple secrets. + Used to print the plain value of a single requested secret without any table style. Default value: `false` @@ -119,10 +175,11 @@ $ infisical secrets get DOMAIN 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. + When running in CI/CD environments or in a script, set `INFISICAL_DISABLE_UPDATE_CHECK=true` or add the `--silent` flag. This will help hide any CLI info/debug output and only show the secret value. + diff --git a/docs/documentation/getting-started/platform.mdx b/docs/documentation/getting-started/platform.mdx index 1a1164a40..7ce96a0ed 100644 --- a/docs/documentation/getting-started/platform.mdx +++ b/docs/documentation/getting-started/platform.mdx @@ -12,14 +12,14 @@ From there, you can invite external members to the organization and start creati ### Projects The **Projects** page shows you all the projects that you have access to within your organization. -Here, you can also create a new project. +Here, you can also create a new project. ![organization overview](../../images/organization-overview.png) ### Members -The **Members** page lets you add or remove external members to your organization. -Note that you can configure your organization in Infisical to have members authenticate with the platform via protocols like SAML 2.0. +The **Members** page lets you add or remove external members to your organization. +Note that you can configure your organization in Infisical to have members authenticate with the platform via protocols like SAML 2.0 and OpenID Connect. ![organization members](../../images/organization/platform/organization-members.png) @@ -35,13 +35,14 @@ The **Secrets Overview** screen provides a bird's-eye view of all the secrets in ![dashboard secrets overview](../../images/dashboard-secrets-overview.png) In the above image, you can already see that: + - `STRIPE_API_KEY` is missing from the **Staging** environment. - `JWT_SECRET` is missing from the **Production** environment. - `BAR` is `EMPTY` in the **Production** environment. ### Dashboard -The secrets dashboard lets you manage secrets for a specific environment in a project. +The secrets dashboard lets you manage secrets for a specific environment in a project. Here, developers can override secrets, version secrets, rollback projects to any point in time and much more. ![dashboard](../../images/dashboard.png) @@ -61,4 +62,4 @@ which you can assign to members. That's it for the platform quickstart! — We encourage you to continue exploring the documentation to gain a deeper understanding of the extensive features and functionalities that Infisical has to offer. -Next, head back to [Getting Started > Introduction](/documentation/getting-started/overview) to explore ways to fetch secrets from Infisical to your apps and infrastructure. \ No newline at end of file +Next, head back to [Getting Started > Introduction](/documentation/getting-started/overview) to explore ways to fetch secrets from Infisical to your apps and infrastructure. diff --git a/docs/documentation/platform/dynamic-secrets/mssql.mdx b/docs/documentation/platform/dynamic-secrets/mssql.mdx new file mode 100644 index 000000000..8dae71399 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -0,0 +1,118 @@ +--- +title: "MS SQL" +description: "How to dynamically generate MS SQL database users." +--- + +The Infisical MS SQL dynamic secret allows you to generate Microsoft SQL server database credentials on demand based on configured role. + +## Prerequisite + +Create a user with the required permission in your SQL instance. This user will be used to create new accounts on-demand. + + +## Set up Dynamic Secrets with MS SQL + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret + + + + Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**. + + + + Database host + + + + Database port + + + + Username that will be used to create dynamic secrets + + + + Password that will be used to create dynamic secrets + + + + Name of the database for which you want to create dynamic secrets + + + + A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions). + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png) + + + + If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s). + + ![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements-mssql.png) + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + If this step fails, you may have to add the CA certficate. + + + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) + + + +## Audit or Revoke Leases +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the expiration time of the lease or delete the lease before it's set time to live. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) + +## Renew Leases +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx index 9cc6c4c3d..2db1834c4 100644 --- a/docs/documentation/platform/identities/machine-identities.mdx +++ b/docs/documentation/platform/identities/machine-identities.mdx @@ -26,13 +26,6 @@ A typical workflow for using identities consists of four steps: 3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back. 4. Authenticating subsequent requests with the Infisical API using the short-lived access token. - - Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, Terraform, Kubernetes Operator, and Infisical Agent. They do not work with clients such as CLI, Ansible look up plugin, etc. - -Machine Identity support for the rest of the clients is planned to be released in the current quarter. - - - ## Authentication Methods To interact with various resources in Infisical, Machine Identities are able to authenticate using: diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx index 5e4253a34..939eaa727 100644 --- a/docs/documentation/platform/ldap/general.mdx +++ b/docs/documentation/platform/ldap/general.mdx @@ -30,6 +30,7 @@ Prerequisites: - 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. - User Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`. + - Unique User Attribute: The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID` ... If left blank, defaults to `uidNumber` - User Search Filter (optional): Template used to construct the LDAP user search filter such as `(uid={{username}})`; use literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas. - Group Search Base / Group DN (optional): LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`. - Group Filter (optional): Template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: [`UserDN`, `UserName`]. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas. diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx index b92b52bb9..39579b785 100644 --- a/docs/documentation/platform/ldap/jumpcloud.mdx +++ b/docs/documentation/platform/ldap/jumpcloud.mdx @@ -39,6 +39,7 @@ Prerequisites: - 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. - User Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=,dc=jumpcloud,dc=com`). + - Unique User Attribute: The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID` ... If left blank, defaults to `uidNumber` - User Search Filter (optional): Template used to construct the LDAP user search filter (`(uid={{username}})`). - Group Search Base / Group DN (optional): LDAP search base to use for group membership search (`ou=Users,o=,dc=jumpcloud,dc=com`). - Group Filter (optional): Template used when constructing the group membership query (`(&(objectClass=groupOfNames)(member=uid={{.Username}},ou=Users,o=,dc=jumpcloud,dc=com))`) diff --git a/docs/documentation/platform/ldap/overview.mdx b/docs/documentation/platform/ldap/overview.mdx index 4d6c75e15..4502158d0 100644 --- a/docs/documentation/platform/ldap/overview.mdx +++ b/docs/documentation/platform/ldap/overview.mdx @@ -14,8 +14,6 @@ then you should contact sales@infisical.com to purchase an enterprise license to 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 nature of authentication in 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 diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx index d45bb6d4f..5e75f1a3b 100644 --- a/docs/documentation/platform/organization.mdx +++ b/docs/documentation/platform/organization.mdx @@ -21,20 +21,19 @@ The **Settings** page lets you manage information about your organization includ ![organization settings general](../../images/platform/organization/organization-settings-general.png) - -- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations. +- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [OIDC](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations. ![organization settings auth](../../images/platform/organization/organization-settings-auth.png) ## Access Control -The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization. +The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization. You can add or remove additional members as well as modify their permissions. ![organization members](../../images/platform/organization/organization-members.png) ![organization identities](../../images/platform/organization/organization-machine-identities.png) -In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization. +In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization. Note that Role-Based Access Management (RBAC) is partly a paid feature. @@ -42,13 +41,14 @@ In the **Organization Roles** tab, you can edit current or create new custom rol Infisical provides immutable roles like `admin`, `member`, etc. at the organization and project level for free. - If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**. - If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. +If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**. +If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. + ![organization roles](../../images/platform/organization/organization-members-roles.png) -As you can see next, Infisical supports granular permissions that you can tailor to each role. +As you can see next, Infisical supports granular permissions that you can tailor to each role. If you need certain members to only be able to access billing details, for example, then you can assign them that permission only. @@ -66,4 +66,4 @@ This includes the following items: - Receipts: The receipts of monthly/annual invoices. - Billing: The billing details of your organization including payment methods on file, tax IDs (if applicable), etc. -![organization usage and billing](../../images/platform/organization/organization-usage-billing.png) \ No newline at end of file +![organization usage and billing](../../images/platform/organization/organization-usage-billing.png) diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index fe5546681..7dc3bca88 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -56,9 +56,9 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. - Issuing CA: The CA under which to issue the certificate. - Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty. - - Common Name (CN): The (common) name of the certificate. + - Common Name (CN): The (common) name for the certificate like `service.acme.com`. + - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`. - TTL: The lifetime of the certificate in seconds. - - Valid Until: The date until which the certificate is valid in the date time string format specified [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). For example, the following formats would be valid: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss.sssZ`. diff --git a/docs/documentation/platform/secret-sharing.mdx b/docs/documentation/platform/secret-sharing.mdx index 680751820..9c0821508 100644 --- a/docs/documentation/platform/secret-sharing.mdx +++ b/docs/documentation/platform/secret-sharing.mdx @@ -5,7 +5,7 @@ description: "Learn how to share time & view-count bound secrets securely with a --- Developers frequently need to share secrets with team members, contractors, or other third parties, which can be risky due to potential leaks or misuse. -Infisical offers a secure solution for sharing secrets over the internet in a time and view count bound manner. +Infisical offers a secure solution for sharing secrets over the internet in a time and view count bound manner. It is possible to share secrets without signing up via [share.infisical.com](https://share.infisical.com) or via Infisical Dashboard (which has more advanced funcitonality). With its zero-knowledge architecture, secrets shared via Infisical remain unreadable even to Infisical itself. diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx new file mode 100644 index 000000000..2b459d5ca --- /dev/null +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -0,0 +1,66 @@ +--- +title: "Auth0 OIDC" +description: "Learn how to configure Auth0 OIDC for Infisical SSO." +--- + + + Auth0 OIDC 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 sales@infisical.com to purchase an enterprise license to use + it. + + + + + 1.1. From the Application's Page, navigate to the settings tab of the Auth0 application you want to integrate with Infisical. + ![OIDC auth0 list of applications](../../../images/sso/auth0-oidc/application-settings.png) + + 1.2. In the Application URIs section, set the **Application Login URI** and **Allowed Web Origins** fields to `https://app.infisical.com` and the **Allowed Callback URL** field to `https://app.infisical.com/api/v1/sso/oidc/callback`. + ![OIDC auth0 create application uris](../../../images/sso/auth0-oidc/application-uris.png) + ![OIDC auth0 create application origin](../../../images/sso/auth0-oidc/application-origin.png) + + If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain. + + + Once done, click **Save Changes**. + + 1.3. Proceed to the Connections Tab and enable desired connections. + ![OIDC auth0 application connections](../../../images/sso/auth0-oidc/application-connections.png) + + + + 2.1. From the application settings page, retrieve the **Client ID** and **Client Secret** + ![OIDC auth0 application credential](../../../images/sso/auth0-oidc/application-credential.png) + + 2.2. In the advanced settings (bottom-most section), retrieve the **OpenID Configuration URL** from the Endpoints tab. + ![OIDC auth0 application oidc url](../../../images/sso/auth0-oidc/application-urls.png) + + Keep these values handy as we will need them in the next steps. + + + + 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Manage**. + ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) + + 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. + ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) + + Once you've done that, press **Update** to complete the required configuration. + + + + Enabling OIDC allows members in your organization to log into Infisical via Auth0. + + ![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png) + + + + + + If you're configuring OIDC 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) + diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx new file mode 100644 index 000000000..ae559cc59 --- /dev/null +++ b/docs/documentation/platform/sso/general-oidc.mdx @@ -0,0 +1,69 @@ +--- +title: "General OIDC" +description: "Learn how to configure OIDC for Infisical SSO with any OIDC-compliant identity provider" +--- + + + OIDC 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 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 through identity providers via [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html). + +Prerequisites: + +- The identity provider (Okta, Google, Azure AD, etc.) should support OIDC. +- Users in the IdP should have a configured `email` and `given_name`. + + + + 1.1. Register your application with the IdP to obtain a **Client ID** and **Client Secret**. These credentials are used by Infisical to authenticate with your IdP. + + 1.2. Configure **Redirect URL** to be `https://app.infisical.com/api/v1/sso/oidc/callback`. If you're self-hosting Infisical, replace the domain with your own. + + 1.3. Configure the scopes needed by Infisical (email, profile, openid) and ensure that they are mapped to the ID token claims. + + 1.4. Access the IdP’s OIDC discovery document (usually located at `https:///.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys. + + + 2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage + ![OIDC general manage org Infisical](../../../images/sso/general-oidc/org-oidc-manage.png) + + 2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints. + + To configure OIDC via Discovery URL, set the **Configuration Type** field to **Discovery URL** and fill out the **Discovery Document URL** field. + + + Note that the Discovery Document URL typically takes the form: `https:///.well-known/openid-configuration`. + + + ![OIDC general discovery config](../../../images/sso/general-oidc/discovery-oidc-form.png) + + To configure OIDC via the custom endpoints, set the **Configuration Type** field to **Custom** and input the required endpoint fields. + ![OIDC general custom config](../../../images/sso/general-oidc/custom-oidc-form.png) + + 2.3. Optionally, you can define a whitelist of allowed email domains. + + Finally, fill out the **Client ID** and **Client Secret** fields and press **Update** to complete the required configuration. + + + + + Enabling OIDC SSO allows members in your organization to log into Infisical via the configured Identity Provider + + ![OIDC general enable OIDC](../../../images/sso/general-oidc/org-oidc-enable.png) + + + + + + + If you're configuring OIDC 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) + diff --git a/docs/documentation/platform/sso/keycloak-oidc.mdx b/docs/documentation/platform/sso/keycloak-oidc.mdx new file mode 100644 index 000000000..d3818f61c --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-oidc.mdx @@ -0,0 +1,92 @@ +--- +title: "Keycloak OIDC" +description: "Learn how to configure Keycloak OIDC for Infisical SSO." +--- + + + Keycloak OIDC 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 sales@infisical.com to purchase an enterprise license to + use it. + + + + + 1.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. + + ![OIDC keycloak list of clients](../../../images/sso/keycloak-oidc/clients-list.png) + + + You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. + + + 1.2. In the General Settings step, set **Client type** to **OpenID Connect**, the **Client ID** field to an appropriate identifier, and the **Name** field to a friendly name like **Infisical**. + + ![OIDC keycloak create client general settings](../../../images/sso/keycloak-oidc/create-client-general-settings.png) + + 1.3. Next, in the Capability Config step, ensure that **Client Authentication** is set to On and that **Standard flow** is enabled in the Authentication flow section. + + ![OIDC keycloak create client capability config settings](../../../images/sso/keycloak-oidc/create-client-capability.png) + + 1.4. In the Login Settings step, set the following values: + - Root URL: `https://app.infisical.com`. + - Home URL: `https://app.infisical.com`. + - Valid Redirect URIs: `https://app.infisical.com/api/v1/sso/oidc/callback`. + - Web origins: `https://app.infisical.com`. + + ![OIDC keycloak create client login settings](../../../images/sso/keycloak-oidc/create-client-login-settings.png) + + If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com (base URL) with your own domain. + + + 1.5. Next, navigate to the **Client scopes** tab and select the client's dedicated scope. + + ![OIDC keycloak client scopes list](../../../images/sso/keycloak-oidc/client-scope-list.png) + + 1.6. Next, click **Add predefined mapper**. + + ![OIDC keycloak client mappers empty](../../../images/sso/keycloak-oidc/client-scope-mapper-menu.png) + + 1.7. Select the **email**, **given name**, **family name** attributes and click **Add**. + + ![OIDC keycloak client mappers predefined 1](../../../images/sso/keycloak-oidc/scope-predefined-mapper-1.png) + ![OIDC keycloak client mappers predefined 2](../../../images/sso/keycloak-oidc/scope-predefined-mapper-2.png) + + Once you've completed the above steps, the list of mappers should look like the following: + ![OIDC keycloak client mappers completed](../../../images/sso/keycloak-oidc/client-scope-complete-overview.png) + + + + 2.1. Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > OpenID Endpoint Configuration and copy the opened URL. This is what is to referred to as the Discovery Document URL and it takes the form: `https://keycloak-mysite.com/realms/myrealm/.well-known/openid-configuration`. + ![OIDC keycloak realm OIDC metadata](../../../images/sso/keycloak-oidc/realm-setting-oidc-config.png) + + 2.2. From the Clients page, navigate to the Credential tab and copy the **Client Secret** to be used in the next steps. + ![OIDC keycloak realm OIDC secret](../../../images/sso/keycloak-oidc/client-secret.png) + + + + 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Manage + ![OIDC keycloak manage org Infisical](../../../images/sso/keycloak-oidc/manage-org-oidc.png) + + 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **Client ID**, and **Client Secret**. + ![OIDC keycloak paste values into Infisical](../../../images/sso/keycloak-oidc/create-oidc.png) + + Once you've done that, press **Update** to complete the required configuration. + + + + Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak. + + ![OIDC keycloak enable OIDC](../../../images/sso/keycloak-oidc/enable-oidc.png) + + + + + + If you're configuring OIDC 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) + diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index 9ab0acc3a..4bb45cf48 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -7,16 +7,14 @@ description: "Learn how to 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 team@infisical.com. + and OpenID Connect (OIDC) 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 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). - -To note, Infisical's SSO implementation decouples the **authentication** and **decryption** steps – which implies that no -Identity Provider can have access to the decryption key needed to decrypt your secrets (this also implies that Infisical requires entering the user's Master Password on top of authenticating with SSO). +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) or [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html). ## Identity providers @@ -30,6 +28,9 @@ Infisical supports these and many other identity providers: - [JumpCloud SAML](/documentation/platform/sso/jumpcloud) - [Keycloak SAML](/documentation/platform/sso/keycloak-saml) - [Google SAML](/documentation/platform/sso/google-saml) +- [Keycloak OIDC](/documentation/platform/sso/keycloak-oidc) +- [Auth0 OIDC](/documentation/platform/sso/auth0-oidc) +- [General OIDC](/documentation/platform/sso/general-oidc) If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance. diff --git a/docs/documentation/platform/webhooks.mdx b/docs/documentation/platform/webhooks.mdx index 22277dd8c..dc3a71b27 100644 --- a/docs/documentation/platform/webhooks.mdx +++ b/docs/documentation/platform/webhooks.mdx @@ -9,7 +9,9 @@ Webhooks can be used to trigger changes to your integrations when secrets are mo To create a webhook for a particular project, go to `Project Settings > Webhooks`. -When creating a webhook, you can specify an environment and folder path (using glob patterns) to trigger only specific integrations. +Infisical supports two webhook types - General and Slack. If you need to integrate with Slack, use the Slack type with an [Incoming Webhook](https://api.slack.com/messaging/webhooks). When creating a webhook, you can specify an environment and folder path to trigger only specific integrations. + +![webhook-create](../../images/webhook-create.png) ## Secret Key Verification @@ -27,7 +29,7 @@ If the signature in the header matches the signature that you generated, then yo { "event": "secret.modified", "project": { - "workspaceId":"the workspace id", + "workspaceId": "the workspace id", "environment": "project environment", "secretPath": "project folder path" }, diff --git a/docs/images/integrations/aws/integration-aws-iam-assume-arn.png b/docs/images/integrations/aws/integration-aws-iam-assume-arn.png new file mode 100644 index 000000000..1c36fc151 Binary files /dev/null and b/docs/images/integrations/aws/integration-aws-iam-assume-arn.png differ diff --git a/docs/images/integrations/aws/integration-aws-iam-assume-permission.png b/docs/images/integrations/aws/integration-aws-iam-assume-permission.png new file mode 100644 index 000000000..0fb8d493d Binary files /dev/null and b/docs/images/integrations/aws/integration-aws-iam-assume-permission.png differ diff --git a/docs/images/integrations/aws/integration-aws-iam-assume-role.png b/docs/images/integrations/aws/integration-aws-iam-assume-role.png new file mode 100644 index 000000000..29094b060 Binary files /dev/null and b/docs/images/integrations/aws/integration-aws-iam-assume-role.png differ diff --git a/docs/images/integrations/aws/integration-aws-iam-assume-select.png b/docs/images/integrations/aws/integration-aws-iam-assume-select.png new file mode 100644 index 000000000..63c5d2b01 Binary files /dev/null and b/docs/images/integrations/aws/integration-aws-iam-assume-select.png differ diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png b/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png index cc17097e1..ae83fcf9a 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png differ diff --git a/docs/images/integrations/bitbucket/integrations-bitbucket-env.png b/docs/images/integrations/bitbucket/integrations-bitbucket-env.png new file mode 100644 index 000000000..8c683a100 Binary files /dev/null and b/docs/images/integrations/bitbucket/integrations-bitbucket-env.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png new file mode 100644 index 000000000..7f296a441 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-mssql.png differ diff --git a/docs/images/platform/dynamic-secrets/modify-sql-statements-mssql.png b/docs/images/platform/dynamic-secrets/modify-sql-statements-mssql.png new file mode 100644 index 000000000..e399db47d Binary files /dev/null and b/docs/images/platform/dynamic-secrets/modify-sql-statements-mssql.png differ diff --git a/docs/images/platform/ldap/ldap-config.png b/docs/images/platform/ldap/ldap-config.png index 2cd711dd1..0ba0b5772 100644 Binary files a/docs/images/platform/ldap/ldap-config.png and b/docs/images/platform/ldap/ldap-config.png differ diff --git a/docs/images/platform/ldap/ldap-test-connection.png b/docs/images/platform/ldap/ldap-test-connection.png index 9f1a3896c..7400aafd5 100644 Binary files a/docs/images/platform/ldap/ldap-test-connection.png and b/docs/images/platform/ldap/ldap-test-connection.png differ diff --git a/docs/images/sso/auth0-oidc/application-connections.png b/docs/images/sso/auth0-oidc/application-connections.png new file mode 100644 index 000000000..8307f3b24 Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-connections.png differ diff --git a/docs/images/sso/auth0-oidc/application-credential.png b/docs/images/sso/auth0-oidc/application-credential.png new file mode 100644 index 000000000..157d82415 Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-credential.png differ diff --git a/docs/images/sso/auth0-oidc/application-origin.png b/docs/images/sso/auth0-oidc/application-origin.png new file mode 100644 index 000000000..82394c6fd Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-origin.png differ diff --git a/docs/images/sso/auth0-oidc/application-settings.png b/docs/images/sso/auth0-oidc/application-settings.png new file mode 100644 index 000000000..5f708ba2a Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-settings.png differ diff --git a/docs/images/sso/auth0-oidc/application-uris.png b/docs/images/sso/auth0-oidc/application-uris.png new file mode 100644 index 000000000..dadc6ce00 Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-uris.png differ diff --git a/docs/images/sso/auth0-oidc/application-urls.png b/docs/images/sso/auth0-oidc/application-urls.png new file mode 100644 index 000000000..b467d54c3 Binary files /dev/null and b/docs/images/sso/auth0-oidc/application-urls.png differ diff --git a/docs/images/sso/auth0-oidc/enable-oidc.png b/docs/images/sso/auth0-oidc/enable-oidc.png new file mode 100644 index 000000000..32682c302 Binary files /dev/null and b/docs/images/sso/auth0-oidc/enable-oidc.png differ diff --git a/docs/images/sso/auth0-oidc/org-oidc-overview.png b/docs/images/sso/auth0-oidc/org-oidc-overview.png new file mode 100644 index 000000000..ce5fb0d60 Binary files /dev/null and b/docs/images/sso/auth0-oidc/org-oidc-overview.png differ diff --git a/docs/images/sso/auth0-oidc/org-update-oidc.png b/docs/images/sso/auth0-oidc/org-update-oidc.png new file mode 100644 index 000000000..0b9e96b5b Binary files /dev/null and b/docs/images/sso/auth0-oidc/org-update-oidc.png differ diff --git a/docs/images/sso/general-oidc/custom-oidc-form.png b/docs/images/sso/general-oidc/custom-oidc-form.png new file mode 100644 index 000000000..2aee02680 Binary files /dev/null and b/docs/images/sso/general-oidc/custom-oidc-form.png differ diff --git a/docs/images/sso/general-oidc/discovery-oidc-form.png b/docs/images/sso/general-oidc/discovery-oidc-form.png new file mode 100644 index 000000000..ae99b35b2 Binary files /dev/null and b/docs/images/sso/general-oidc/discovery-oidc-form.png differ diff --git a/docs/images/sso/general-oidc/org-oidc-enable.png b/docs/images/sso/general-oidc/org-oidc-enable.png new file mode 100644 index 000000000..69f0062bc Binary files /dev/null and b/docs/images/sso/general-oidc/org-oidc-enable.png differ diff --git a/docs/images/sso/general-oidc/org-oidc-manage.png b/docs/images/sso/general-oidc/org-oidc-manage.png new file mode 100644 index 000000000..69a60f0f5 Binary files /dev/null and b/docs/images/sso/general-oidc/org-oidc-manage.png differ diff --git a/docs/images/sso/keycloak-oidc/client-scope-complete-overview.png b/docs/images/sso/keycloak-oidc/client-scope-complete-overview.png new file mode 100644 index 000000000..a0965db0b Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-scope-complete-overview.png differ diff --git a/docs/images/sso/keycloak-oidc/client-scope-list.png b/docs/images/sso/keycloak-oidc/client-scope-list.png new file mode 100644 index 000000000..c35a7691f Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-scope-list.png differ diff --git a/docs/images/sso/keycloak-oidc/client-scope-mapper-menu.png b/docs/images/sso/keycloak-oidc/client-scope-mapper-menu.png new file mode 100644 index 000000000..141bc5dd0 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-scope-mapper-menu.png differ diff --git a/docs/images/sso/keycloak-oidc/client-secret.png b/docs/images/sso/keycloak-oidc/client-secret.png new file mode 100644 index 000000000..c91ddb164 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/client-secret.png differ diff --git a/docs/images/sso/keycloak-oidc/clients-list.png b/docs/images/sso/keycloak-oidc/clients-list.png new file mode 100644 index 000000000..50e4e49cb Binary files /dev/null and b/docs/images/sso/keycloak-oidc/clients-list.png differ diff --git a/docs/images/sso/keycloak-oidc/create-client-capability.png b/docs/images/sso/keycloak-oidc/create-client-capability.png new file mode 100644 index 000000000..72aa50850 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-client-capability.png differ diff --git a/docs/images/sso/keycloak-oidc/create-client-general-settings.png b/docs/images/sso/keycloak-oidc/create-client-general-settings.png new file mode 100644 index 000000000..87ec8d837 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-client-general-settings.png differ diff --git a/docs/images/sso/keycloak-oidc/create-client-login-settings.png b/docs/images/sso/keycloak-oidc/create-client-login-settings.png new file mode 100644 index 000000000..1c839f8d4 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-client-login-settings.png differ diff --git a/docs/images/sso/keycloak-oidc/create-oidc.png b/docs/images/sso/keycloak-oidc/create-oidc.png new file mode 100644 index 000000000..358af1330 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/create-oidc.png differ diff --git a/docs/images/sso/keycloak-oidc/enable-oidc.png b/docs/images/sso/keycloak-oidc/enable-oidc.png new file mode 100644 index 000000000..6518319c4 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/enable-oidc.png differ diff --git a/docs/images/sso/keycloak-oidc/manage-org-oidc.png b/docs/images/sso/keycloak-oidc/manage-org-oidc.png new file mode 100644 index 000000000..e6346cd24 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/manage-org-oidc.png differ diff --git a/docs/images/sso/keycloak-oidc/realm-setting-oidc-config.png b/docs/images/sso/keycloak-oidc/realm-setting-oidc-config.png new file mode 100644 index 000000000..9d3866c7e Binary files /dev/null and b/docs/images/sso/keycloak-oidc/realm-setting-oidc-config.png differ diff --git a/docs/images/sso/keycloak-oidc/scope-predefined-mapper-1.png b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-1.png new file mode 100644 index 000000000..8b1cb16c4 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-1.png differ diff --git a/docs/images/sso/keycloak-oidc/scope-predefined-mapper-2.png b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-2.png new file mode 100644 index 000000000..189cb34b2 Binary files /dev/null and b/docs/images/sso/keycloak-oidc/scope-predefined-mapper-2.png differ diff --git a/docs/images/webhook-create.png b/docs/images/webhook-create.png new file mode 100644 index 000000000..e73f14767 Binary files /dev/null and b/docs/images/webhook-create.png differ diff --git a/docs/integrations/cicd/bitbucket.mdx b/docs/integrations/cicd/bitbucket.mdx index 8f29e43a5..2aa2106da 100644 --- a/docs/integrations/cicd/bitbucket.mdx +++ b/docs/integrations/cicd/bitbucket.mdx @@ -7,26 +7,62 @@ Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - Navigate to your project's integrations tab in Infisical. + + + + + Navigate to your project's integrations tab in Infisical. - ![integrations](../../images/integrations.png) + ![integrations](../../images/integrations.png) - Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. + Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. - ![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png) + ![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png) - - If this is your project's first cloud integration, then you'll have to grant - Infisical access to your project's environment variables. Although this step - breaks E2EE, it's necessary for Infisical to sync the environment variables to - the cloud platform. - - - - Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo. + + + Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo. - ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png) - - \ No newline at end of file + ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png) + + + + + + + + Configure a [Machine Identity](https://infisical.com/docs/documentation/platform/identities/universal-auth) for your project and give it permissions to read secrets from your desired Infisical projects and environments. + + + Create Bitbucket variables (can be either workspace, repository, or deployment-level) to store Machine Identity Client ID and Client Secret. + + ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket-env.png) + + + Edit your Bitbucket pipeline YAML file to include the use of the Infisical CLI to fetch and inject secrets into any script or command within the pipeline. + + #### Example + + ```yaml + image: atlassian/default-image:3 + + pipelines: + default: + - step: + name: Build application with secrets from Infisical + script: + - apt update && apt install -y curl + - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash + - apt-get update && apt-get install -y infisical + - export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain) + - infisical run --projectId=1d0443c1-cd43-4b3a-91a3-9d5f81254a89 --env=dev -- npm run build + ``` + + + Set the values of `projectId` and `env` flags in the `infisical run` command to your intended source path. For more options, refer to the CLI command reference [here](https://infisical.com/docs/cli/commands/run). + + + + + + diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index 976c81a6d..c6d72a11e 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -77,11 +77,12 @@ description: "How to sync secrets from Infisical to GitLab" + Using the GitLab integration on a self-hosted instance of Infisical requires configuring an application in GitLab and registering your instance with it. - + If you're self-hosting Gitlab with custom certificates, you will have to configure your Infisical instance to trust these certificates. To learn how, please follow [this guide](../../self-hosting/guides/custom-certificates). Navigate to your user Settings > Applications to create a new GitLab application. @@ -91,8 +92,8 @@ description: "How to sync secrets from Infisical to GitLab" Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/gitlab/oauth2/callback`. - ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-new-app-form.png) - + ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-new-app-form.png) + If you have a GitLab group, you can create an OAuth application under it in your group Settings > Applications. @@ -100,17 +101,17 @@ description: "How to sync secrets from Infisical to GitLab" Obtain the **Application ID** and **Secret** for your GitLab application. - - ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-credentials.png) - + + ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-credentials.png) + Back in your Infisical instance, add two new environment variables for the credentials of your GitLab application: - `CLIENT_ID_GITLAB`: The **Client ID** of your GitLab application. - `CLIENT_SECRET_GITLAB`: The **Secret** of your GitLab application. - + Once added, restart your Infisical instance and use the GitLab integration. + - diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 9b3a8a2f8..64df1df32 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -3,6 +3,156 @@ title: "AWS Secrets Manager" description: "Learn how to sync secrets from Infisical to AWS Secrets Manager." --- + + +Infisical will assume the provided role in your AWS account securely, without the need to share any credentials. + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + + + To connect your Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the AWS IAM Role for the integration. + +If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. + +The following steps are for instances not deployed on AWS + + + Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. + + + Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAssumeAnyRole", + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/*" + } + ] +} +``` + + + Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys. + + ![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png) + ![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png) + ![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png) + + + 1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**. + 2. Set the secret key as **CLIENT_SECRET_AWS_INTEGRATION**. + + + + + + + 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. + ![IAM Role Creation](../../images/integrations/aws/integration-aws-iam-assume-role.png) + + 2. Select **AWS Account** as the **Trusted Entity Type**. + 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. + 4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security. + + + + ![IAM Role Permissions](../../images/integrations/aws/integration-aws-iam-assume-permission.png) + Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSecretsManagerAccess", + "Effect": "Allow", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:CreateSecret", + "secretsmanager:UpdateSecret", + "secretsmanager:DescribeSecret", + "secretsmanager:TagResource", + "secretsmanager:UntagResource", + "kms:ListKeys", + "kms:ListAliases", + "kms:Encrypt", + "kms:Decrypt" + ], + "Resource": "*" + } + ] + } + ``` + + + + ![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) + + + + 1. Navigate to your project's integrations tab in Infisical. + 2. Click on the **AWS Secrets Manager** tile. + ![Select AWS Secrets Manager](../../images/integrations.png) + + 3. Select the **AWS Assume Role** option. + ![Select Assume Role](../../images/integrations/aws/integration-aws-iam-assume-select.png) + + 4. Provide the **AWS IAM Role ARN** obtained from the previous step. + + Select how you want to integration to work by specifying a number of parameters: + + + The environment in Infisical from which you want to sync secrets to AWS Secrets Manager. + + + The path within the preselected environment form which you want to sync secrets to AWS Secrets Manager. + + + The region that you want to integrate with in AWS Secrets Manager. + + + How you want the integration to map the secrets. The selected value could be either one to one or one to many. + + + The secret name/path in AWS into which you want to sync the secrets from Infisical. + + + ![integration create](../../images/integrations/aws/integrations-aws-secret-manager-create.png) + + Optionally, you can add tags or specify the encryption key of all the secrets created via this integration: + + + The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API. + + + The alias/ID of the AWS KMS key used for encryption. Please note that key should be enabled in order to work and the IAM user should have access to it. + + ![integration options](../../images/integrations/aws/integrations-aws-secret-manager-options.png) + + Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager. + + + Infisical currently syncs environment variables to AWS Secrets Manager as + key-value pairs under one secret. We're actively exploring ways to help users + group environment variable key-pairs under multiple secrets for greater + control. + + + Please note that upon deleting secrets in Infisical, AWS Secrets Manager immediately makes the secrets inaccessible but only schedules them for deletion after at least 7 days. + + + + + + +Infisical will access your account using the provided AWS access key and secret key. + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) @@ -51,13 +201,13 @@ Prerequisites: ![access key 2](../../images/integrations/aws/integrations-aws-access-key-2.png) ![access key 3](../../images/integrations/aws/integrations-aws-access-key-3.png) - Navigate to your project's integrations tab in Infisical. + 1. Navigate to your project's integrations tab in Infisical. + 2. Click on the **AWS Secrets Manager** tile. + ![Select AWS Secrets Manager](../../images/integrations.png) - ![integrations](../../images/integrations.png) - - Press on the AWS Secrets Manager tile and input your AWS access key ID and secret access key from the previous step. - - ![integration auth](../../images/integrations/aws/integrations-aws-secret-manager-auth.png) + 3. Select the **Access Key** option for Authentication Mode. + ![Select Access Key](../../images/integrations/aws/integrations-aws-secret-manager-auth.png) + 4. Provide the **access key** and **secret key** for the AWS Iam User. @@ -105,3 +255,5 @@ Prerequisites: + + diff --git a/docs/integrations/cloud/azure-key-vault.mdx b/docs/integrations/cloud/azure-key-vault.mdx index 3a4b68e39..6b43a7b0f 100644 --- a/docs/integrations/cloud/azure-key-vault.mdx +++ b/docs/integrations/cloud/azure-key-vault.mdx @@ -29,12 +29,6 @@ description: "How to sync secrets from Infisical to Azure Key Vault" ![integrations](../../images/integrations/azure-key-vault/integrations-azure-key-vault.png) - - If this is your project's first cloud integration, then you'll have to grant - Infisical access to your project's environment variables. Although this step - breaks E2EE, it's necessary for Infisical to sync the environment variables to - the cloud platform. - diff --git a/docs/integrations/cloud/terraform-cloud.mdx b/docs/integrations/cloud/terraform-cloud.mdx index 3fd70df73..d68e8a14f 100644 --- a/docs/integrations/cloud/terraform-cloud.mdx +++ b/docs/integrations/cloud/terraform-cloud.mdx @@ -27,12 +27,6 @@ Prerequisites: ![integrations terraform cloud authorization](../../images/integrations/terraform/integrations-terraformcloud-auth.png) - - If this is your project's first cloud integration, then you'll have to grant - Infisical access to your project's environment variables. Although this step - breaks E2EE, it's necessary for Infisical to sync the environment variables to - the cloud platform. - Select which Infisical environment secrets and Terraform Cloud variable type you want to sync to which Terraform Cloud workspace/project and press create integration to start syncing secrets to Terraform Cloud. @@ -40,4 +34,4 @@ Prerequisites: ![integrations terraform cloud](../../images/integrations/terraform/integrations-terraformcloud-create.png) ![integrations terraform cloud](../../images/integrations/terraform/integrations-terraformcloud.png) - \ No newline at end of file + diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx index a88c6616b..1cb1c06c3 100644 --- a/docs/integrations/cloud/vercel.mdx +++ b/docs/integrations/cloud/vercel.mdx @@ -17,13 +17,6 @@ description: "How to sync secrets from Infisical to Vercel" Press on the Vercel tile and grant Infisical access to your Vercel account. ![integrations vercel authorization](../../images/integrations/vercel/integrations-vercel-auth.png) - - - If this is your project's first cloud integration, then you'll have to grant - Infisical access to your project's environment variables. Although this step - breaks E2EE, it's necessary for Infisical to sync the environment variables to - the cloud platform. - Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press create integration to start syncing secrets to Vercel. diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx index 1b0fb9f32..02b6fd9fb 100644 --- a/docs/internals/security.mdx +++ b/docs/internals/security.mdx @@ -79,10 +79,9 @@ Infisical uses AES-256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 By default, Infisical employs a zero-knowledge-first approach to securely storing and sharing secrets. - Each secret belongs to a project and is symmetrically encrypted by that project's unique key. Each member of a project is shared a copy of the project key, encrypted under their public key, when they are first invited to join the project. -Since these encryption operations occur on the client-side, the Infisical API is not able to view the value of any secret and the default zero-knowledge property of Infisical is retained; as you'd expect, it follows that decryption operations also occur on the client-side. + Since these encryption operations occur on the client-side, the Infisical API is not able to view the value of any secret and the default zero-knowledge property of Infisical is retained; as you'd expect, it follows that decryption operations also occur on the client-side. - An exception to the zero-knowledge property occurs when a member of a project explicitly shares that project's unique key with Infisical. It is often necessary to share the project key with Infisical in order to use features like native integrations and secret rotation that wouldn't be possible to offer otherwise. - ## Infrastructure ### High availability @@ -90,19 +89,22 @@ Since these encryption operations occur on the client-side, the Infisical API is Infisical Cloud utilizes several strategies to ensure high availability, leveraging AWS services to maintain continuous operation and data integrity. #### Multi-AZ AWS RDS -Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments. -This configuration ensures that the database service is highly available and durable. -AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ). -This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption. + +Infisical Cloud uses AWS Relational Database Service (RDS) with Multi-AZ deployments. +This configuration ensures that the database service is highly available and durable. +AWS RDS automatically provisions and maintains a synchronous standby replica of the database in a different Availability Zone (AZ). +This setup facilitates immediate failover to the standby in the event of an AZ failure, thereby ensuring that database operations can continue with minimal interruption. The continuous backup and replication to the standby instance safeguard data against loss and ensure its availability even during system failures. #### Multi-AZ ECS for Container Orchestration -Infisical Cloud leverages Amazon Elastic Container Service (ECS) in a Multi-AZ configuration for container orchestration. -This arrangement enables the management and operation of containers across multiple availability zones, increasing the application's fault tolerance. -Should there be an AZ failure, load is seamlessly sent to an operational AZ, thus minimizing downtime and preserving service availability. + +Infisical Cloud leverages Amazon Elastic Container Service (ECS) in a Multi-AZ configuration for container orchestration. +This arrangement enables the management and operation of containers across multiple availability zones, increasing the application's fault tolerance. +Should there be an AZ failure, load is seamlessly sent to an operational AZ, thus minimizing downtime and preserving service availability. #### Standby Regions for Regional Failover -To fight regional outages, secondary regions are always in standby mode and maintained with up-to-date configurations and data, ready to take over in case the primary region fails. + +To fight regional outages, secondary regions are always in standby mode and maintained with up-to-date configurations and data, ready to take over in case the primary region fails. The standby regions enable a rapid transition and service continuity with minimal disruption in the event of a complete regional failure, ensuring that Infisical Cloud services remain accessible. ### Snapshots @@ -127,7 +129,7 @@ JWT tokens are stored in browser memory and appended to outbound requests requir ### User authentication -Infisical supports several authentication methods including email/password, Google SSO, GitHub SSO, and SAML 2.0 (Okta, Azure, JumpCloud); Infisical also currently offers email-based 2FA with authenticator app methods coming in Q1 2024. +Infisical supports several authentication methods including email/password, Google SSO, GitHub SSO, SAML 2.0 (Okta, Azure, JumpCloud), and OpenID Connect; Infisical also currently offers email-based 2FA with authenticator app methods coming in Q1 2024. Infisical uses the [secure remote password protocol](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol#:~:text=The%20SRP%20protocol%20has%20a,the%20user%20to%20the%20server), commonly found in other zero-knowledge platform architectures, for authentication. Put simply, the protocol enables Infisical to validate a user's knowledge of their password without ever seeing it by constructing a mutual secret; we use this protocol because each user's password is used to seed the generation of a master encryption/decryption key via KDF for that user which the platform @@ -141,6 +143,7 @@ Lastly, Infisical enforces strong password requirements according to the guidanc to access the platform. We strongly encourage users to generate and store their passwords / master decryption key in a password manager, such as 1Password, Bitwarden, or Dashlane. + ## Role-based access control (RBAC) @@ -172,7 +175,7 @@ Please email security@infisical.com to request any reports including a letter of Whether or not Infisical or your employees can access data in the Infisical instance and/or storage backend depends on many factors how you use Infisical: - Infisical Self-Hosted: Self-hosting Infisical is common amongst organizations that prefer to keep data on their own infrastructure usually to adhere to strict regulatory and compliance requirements. In this option, organizations retain full control over their data and therefore govern the data access policy of their Infisical instance and storage backend. -- Infisical Cloud: Using Infisical's managed service, [Infisical Cloud](https://app.infisical.com) means delegating data oversight and management to Infisical. Under our policy controls, employees are only granted access to parts of infrastructure according to principle of least privilege; this is especially relevant to customer data can only be accessed currently by executive management of Infisical. Moreover, any changes to sensitive customer data is prohibited without explicit customer approval. +- Infisical Cloud: Using Infisical's managed service, [Infisical Cloud](https://app.infisical.com) means delegating data oversight and management to Infisical. Under our policy controls, employees are only granted access to parts of infrastructure according to principle of least privilege; this is especially relevant to customer data can only be accessed currently by executive management of Infisical. Moreover, any changes to sensitive customer data is prohibited without explicit customer approval. It should be noted that, even on Infisical Cloud, it is physically impossible for employees of Infisical to view the values of secrets if users have not explicitly granted Infisical access to their project (i.e. opted out of zero-knowledge). diff --git a/docs/mint.json b/docs/mint.json index 1caad0715..cb08aab32 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -148,6 +148,7 @@ "documentation/platform/dynamic-secrets/overview", "documentation/platform/dynamic-secrets/postgresql", "documentation/platform/dynamic-secrets/mysql", + "documentation/platform/dynamic-secrets/mssql", "documentation/platform/dynamic-secrets/oracle", "documentation/platform/dynamic-secrets/cassandra", "documentation/platform/dynamic-secrets/aws-iam" @@ -178,7 +179,10 @@ "documentation/platform/sso/azure", "documentation/platform/sso/jumpcloud", "documentation/platform/sso/keycloak-saml", - "documentation/platform/sso/google-saml" + "documentation/platform/sso/google-saml", + "documentation/platform/sso/keycloak-oidc", + "documentation/platform/sso/auth0-oidc", + "documentation/platform/sso/general-oidc" ] }, { @@ -219,7 +223,8 @@ "group": "Guides", "pages": [ "self-hosting/configuration/schema-migrations", - "self-hosting/guides/mongo-to-postgres" + "self-hosting/guides/mongo-to-postgres", + "self-hosting/guides/custom-certificates" ] }, { @@ -416,7 +421,9 @@ "pages": [ "api-reference/endpoints/identities/create", "api-reference/endpoints/identities/update", - "api-reference/endpoints/identities/delete" + "api-reference/endpoints/identities/delete", + "api-reference/endpoints/identities/get-by-id", + "api-reference/endpoints/identities/list" ] }, { @@ -426,9 +433,11 @@ "api-reference/endpoints/universal-auth/attach", "api-reference/endpoints/universal-auth/retrieve", "api-reference/endpoints/universal-auth/update", + "api-reference/endpoints/universal-auth/revoke", "api-reference/endpoints/universal-auth/create-client-secret", "api-reference/endpoints/universal-auth/list-client-secrets", "api-reference/endpoints/universal-auth/revoke-client-secret", + "api-reference/endpoints/universal-auth/get-client-secret-by-id", "api-reference/endpoints/universal-auth/renew-access-token", "api-reference/endpoints/universal-auth/revoke-access-token" ] @@ -638,5 +647,10 @@ ], "integrations": { "intercom": "hsg644ru" + }, + "analytics": { + "koala": { + "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d" + } } } diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index 578e8ad0f..3f779f187 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -19,6 +19,9 @@ From local development to production, Infisical SDKs provide the easiest way for Manage secrets for your Java application on demand + + Manage secrets for your Go application on demand + Manage secrets for your C#/.NET application on demand @@ -43,7 +46,4 @@ From local development to production, Infisical SDKs provide the easiest way for Note: The exact parameter name may differ depending on the language. - - The SDK caches every secret and falls back to the cached value if a request fails. If no cached value is found, and the request fails, then the SDK throws an error. - diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 754409dfe..03bea6fb0 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -47,6 +47,25 @@ The platform utilizes Postgres to persist all of its data and Redis for caching Redis connection string. + + Postgres database read replica connection strings. It accepts a JSON string. +``` +DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}] +``` + + + Postgres read replica connection string. + + + Configure the SSL certificate for securing a Postgres replica connection by first encoding it in base64. + Use the command below to encode your certificate: + `echo "" | base64` + + If not provided it will use master SSL certificate. + + + + ## 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. @@ -445,6 +464,16 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I + + + The AWS IAM User access key for assuming roles. + + + + The AWS IAM User secret key for assuming roles. + + + OAuth2 client id for Azure integration diff --git a/docs/self-hosting/deployment-options/docker-swarm.mdx b/docs/self-hosting/deployment-options/docker-swarm.mdx index c63aff23b..0a447447a 100644 --- a/docs/self-hosting/deployment-options/docker-swarm.mdx +++ b/docs/self-hosting/deployment-options/docker-swarm.mdx @@ -82,6 +82,13 @@ The [Docker stack file](https://github.com/Infisical/infisical/tree/main/docker- ## Deployment instructions + + Run the following on each node to install the Docker engine. + + ``` + curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh + ``` + ``` docker swarm init @@ -161,7 +168,12 @@ The [Docker stack file](https://github.com/Infisical/infisical/tree/main/docker- Run the schema migration to initialize the database. Follow the [guide here](/self-hosting/configuration/schema-migrations) to learn how. - To connect to the Postgres database, use the following default credentials defined in the Docker swarm: username: `postgres`, password: `postgres` and database: `postgres`. + To run the migrations, you'll need to connect to the Postgres instance deployed on your Docker swarm. The default Postgres user credentials are defined in the Docker swarm: username: `postgres`, password: `postgres` and database: `postgres`. + We recommend you change these credentials when deploying to production and creating a separate DB for Infisical. + + + After running the schema migrations, be sure to update the `.env` file to have the correct `DB_CONNECTION_URI`. + diff --git a/docs/self-hosting/guides/custom-certificates.mdx b/docs/self-hosting/guides/custom-certificates.mdx new file mode 100644 index 000000000..67b258d08 --- /dev/null +++ b/docs/self-hosting/guides/custom-certificates.mdx @@ -0,0 +1,26 @@ +--- +title: "Adding Custom Certificates" +description: "Learn how to configure Infisical with custom certificates" +--- + +By default, the Infisical Docker image includes certificates from well-known public certificate authorities. +However, some integrations with Infisical may need to communicate with your internal services that use private certificate authorities. +To configure trust for custom certificates, follow these steps. This is particularly useful for connecting Infisical with self-hosted services like GitLab. + +## Prerequisites + +- Docker +- Standalone [Infisical image](https://hub.docker.com/r/infisical/infisical) +- Certificate public key `.pem` files + +## Setup + +1. Place all your public key `.pem` files into a single directory. +2. Mount the directory containing the `.pem` files to the `usr/local/share/ca-certificates/` path in the Infisical container. +3. Set the following environment variable on your Infisical container: + ``` + NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt + ``` +4. Start the Infisical container. + +By following these steps, your Infisical container will trust the specified certificates, allowing you to securely connect Infisical to your internal services. diff --git a/docs/style.css b/docs/style.css index 3359151e4..4d9877c6c 100644 --- a/docs/style.css +++ b/docs/style.css @@ -10,7 +10,6 @@ #sidebar { left: 0; - padding-left: 48px; padding-right: 30px; border-right: 1px; border-color: #cdd64b; @@ -18,6 +17,10 @@ border-right: 1px solid #ebebeb; } +#sidebar-content { + padding-left: 2rem; +} + #sidebar .relative .sticky { opacity: 0; } @@ -154,4 +157,4 @@ .flex-1 .flex .items-center { /* background-color: #f5f5f5; */ -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b79dd8815..5837e8d7e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "frontend", "dependencies": { "@casl/ability": "^6.5.0", "@casl/react": "^3.1.0", @@ -64,6 +63,7 @@ "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.2.0", "infisical-node": "^1.0.37", + "ip": "^2.0.1", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "jwt-decode": "^3.1.2", @@ -15767,8 +15767,7 @@ "node_modules/ip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" }, "node_modules/ipaddr.js": { "version": "1.9.1", diff --git a/frontend/package.json b/frontend/package.json index a4acb5738..fa3033073 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -71,6 +71,7 @@ "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.2.0", "infisical-node": "^1.0.37", + "ip": "^2.0.1", "jspdf": "^2.5.1", "jsrp": "^0.2.4", "jwt-decode": "^3.1.2", diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index cc26e5512..f9285012e 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -10,7 +10,7 @@ export const initPostHog = () => { try { if (typeof window !== "undefined") { // @ts-ignore - if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") { + if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) { posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_HOST }); diff --git a/frontend/src/components/dashboard/DropZone.tsx b/frontend/src/components/dashboard/DropZone.tsx index ba0b9efdb..b196e8cd4 100644 --- a/frontend/src/components/dashboard/DropZone.tsx +++ b/frontend/src/components/dashboard/DropZone.tsx @@ -6,6 +6,8 @@ import { faUpload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { parseDocument, Scalar, YAMLMap } from "yaml"; +import { SecretType } from "@app/hooks/api/types"; + import Button from "../basic/buttons/Button"; import Error from "../basic/Error"; import { createNotification } from "../notifications"; @@ -33,7 +35,6 @@ const DropZone = ({ numCurrentRows }: DropZoneProps) => { const { t } = useTranslation(); - const handleDragEnter = (e: DragEvent) => { e.preventDefault(); @@ -66,7 +67,7 @@ const DropZone = ({ key, value: keyPairs[key as keyof typeof keyPairs].value, comment: keyPairs[key as keyof typeof keyPairs].comments.join("\n"), - type: "shared", + type: SecretType.Shared, tags: [] })); break; @@ -79,7 +80,7 @@ const DropZone = ({ key, value: keyPairs[key as keyof typeof keyPairs], comment: "", - type: "shared", + type: SecretType.Shared, tags: [] })); break; @@ -102,7 +103,7 @@ const DropZone = ({ key, value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? "", comment, - type: "shared", + type: SecretType.Shared, tags: [] }; }); @@ -132,7 +133,7 @@ const DropZone = ({ if (file === undefined) { createNotification({ text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.", - type: "error", + type: "error" }); setLoading(false); return; diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx index 0d6b0d061..befe79e4f 100644 --- a/frontend/src/components/notifications/Notifications.tsx +++ b/frontend/src/components/notifications/Notifications.tsx @@ -26,4 +26,4 @@ export const createNotification = ( type: myProps?.type || "info", }); -export const NotificationContainer = () => ; +export const NotificationContainer = () => ; diff --git a/frontend/src/components/signup/InitialSignupStep.tsx b/frontend/src/components/signup/InitialSignupStep.tsx index e5c23f333..3a7c6db04 100644 --- a/frontend/src/components/signup/InitialSignupStep.tsx +++ b/frontend/src/components/signup/InitialSignupStep.tsx @@ -4,6 +4,9 @@ import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons import { faEnvelope } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useServerConfig } from "@app/context"; +import { LoginMethod } from "@app/hooks/api/admin/types"; + import { Button } from "../v2"; export default function InitialSignupStep({ @@ -12,67 +15,79 @@ export default function InitialSignupStep({ setIsSignupWithEmail: (value: boolean) => void; }) { const { t } = useTranslation(); + const { config } = useServerConfig(); + + const shouldDisplaySignupMethod = (method: LoginMethod) => + !config.enabledLoginMethods || config.enabledLoginMethods.includes(method); return (

{t("signup.initial-title")}

-
- -
-
- -
-
- -
-
- -
+ {shouldDisplaySignupMethod(LoginMethod.GOOGLE) && ( +
+ +
+ )} + {shouldDisplaySignupMethod(LoginMethod.GITHUB) && ( +
+ +
+ )} + {shouldDisplaySignupMethod(LoginMethod.GITLAB) && ( +
+ +
+ )} + {shouldDisplaySignupMethod(LoginMethod.EMAIL) && ( +
+ +
+ )}
{t("signup.create-policy")}
diff --git a/frontend/src/components/utilities/secrets/checkOverrides.ts b/frontend/src/components/utilities/secrets/checkOverrides.ts index 84ba0bbb7..7f311a252 100644 --- a/frontend/src/components/utilities/secrets/checkOverrides.ts +++ b/frontend/src/components/utilities/secrets/checkOverrides.ts @@ -1,5 +1,7 @@ import { SecretDataProps } from "public/data/frequentInterfaces"; +import { SecretType } from "@app/hooks/api/types"; + /** * This function downloads the secrets as a .env file * @param {object} obj @@ -10,8 +12,8 @@ const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => { let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret)); const overridenSecrets = data!.filter((secret) => secret.valueOverride === undefined || secret?.value !== secret?.valueOverride - ? "shared" - : "personal" + ? SecretType.Shared + : SecretType.Personal ); if (overridenSecrets.length) { overridenSecrets.forEach((secret) => { diff --git a/frontend/src/components/utilities/secrets/encryptSecrets.ts b/frontend/src/components/utilities/secrets/encryptSecrets.ts index 943502cdf..4eb59ae9f 100644 --- a/frontend/src/components/utilities/secrets/encryptSecrets.ts +++ b/frontend/src/components/utilities/secrets/encryptSecrets.ts @@ -3,6 +3,7 @@ import crypto from "crypto"; import { SecretDataProps, Tag } from "public/data/frequentInterfaces"; import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; +import { SecretType } from "@app/hooks/api/types"; import { decryptAssymmetric, encryptSymmetric } from "../cryptography/crypto"; @@ -20,7 +21,7 @@ interface EncryptedSecretProps { secretValueCiphertext: string; secretValueIV: string; secretValueTag: string; - type: "personal" | "shared"; + type: SecretType; tags: Tag[]; } @@ -108,8 +109,8 @@ const encryptSecrets = async ({ secretCommentTag, type: secret.valueOverride === undefined || secret?.value !== secret?.valueOverride - ? "shared" - : "personal", + ? SecretType.Shared + : SecretType.Personal, tags: secret.tags }; diff --git a/frontend/src/components/utilities/telemetry/Telemetry.ts b/frontend/src/components/utilities/telemetry/Telemetry.ts index 6f6fc2367..860314d16 100644 --- a/frontend/src/components/utilities/telemetry/Telemetry.ts +++ b/frontend/src/components/utilities/telemetry/Telemetry.ts @@ -13,7 +13,7 @@ class Capturer { } capture(item: string) { - if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") { + if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) { try { this.api.capture(item); } catch (error) { @@ -23,7 +23,7 @@ class Capturer { } identify(id: string, email?: string) { - if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") { + if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) { try { this.api.identify(id, { email: email diff --git a/frontend/src/components/v2/Alert/Alert.tsx b/frontend/src/components/v2/Alert/Alert.tsx index c43857da1..3d2dd6393 100644 --- a/frontend/src/components/v2/Alert/Alert.tsx +++ b/frontend/src/components/v2/Alert/Alert.tsx @@ -81,7 +81,7 @@ const AlertDescription = forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
+
)); AlertDescription.displayName = "AlertDescription"; diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 5e7bbef78..4ba089c46 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -15,7 +15,7 @@ const replaceContentWithDot = (str: string) => { }; const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => { - if (isImport) return "IMPORTED"; + if (isImport && !content) return "IMPORTED"; if (content === "") return "EMPTY"; if (!content) return "EMPTY"; if (!isVisible) return replaceContentWithDot(content); diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 29dba23c7..015b16f98 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -36,61 +36,73 @@ export const Select = forwardRef( ref ): JSX.Element => { return ( - - - - {props.icon ? : placeholder} - +
+ { + if (!props.onValueChange) return; - - - - - - + - -
- -
-
- - {isLoading ? ( -
- - Loading... -
- ) : ( - children +
+ {props.icon && } + +
+ + + + +
+ + - -
- -
-
-
-
-
+ position={position} + style={{ width: "var(--radix-select-trigger-width)" }} + > + +
+ +
+
+ + {isLoading ? ( +
+ + Loading... +
+ ) : ( + children + )} +
+ +
+ +
+
+ + + +
); } ); @@ -114,7 +126,7 @@ export const SelectItem = forwardRef( outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`, isSelected && "bg-primary", isDisabled && - "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", + "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", className )} ref={forwardedRef} @@ -129,3 +141,45 @@ export const SelectItem = forwardRef( ); SelectItem.displayName = "SelectItem"; + +export type SelectClearProps = Omit & { + onClear: () => void; + selectValue: string; +}; + +export const SelectClear = forwardRef( + ( + { children, className, isSelected, isDisabled, onClear, selectValue, ...props }, + forwardedRef + ) => { + return ( + onClear()} + onClick={() => onClear()} + className={twMerge( + `relative mb-0.5 flex + cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm + outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`, + isSelected && "bg-primary", + isDisabled && + "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", + className + )} + ref={forwardedRef} + > +
+ +
+ {children} +
+ ); + } +); +SelectClear.displayName = "SelectClear"; diff --git a/frontend/src/components/v2/Select/index.tsx b/frontend/src/components/v2/Select/index.tsx index 6a783605a..3765851d5 100644 --- a/frontend/src/components/v2/Select/index.tsx +++ b/frontend/src/components/v2/Select/index.tsx @@ -1,2 +1,2 @@ export type { SelectItemProps, SelectProps } from "./Select"; -export { Select, SelectItem } from "./Select"; +export { Select, SelectClear, SelectItem } from "./Select"; diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index e1c4301d4..fc9fb2c24 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,2 +1,2 @@ -export { useCreateAdminUser, useUpdateServerConfig } from "./mutation"; -export { useGetServerConfig } from "./queries"; +export { useAdminDeleteUser, useCreateAdminUser, useUpdateServerConfig } from "./mutation"; +export { useAdminGetUsers, useGetServerConfig } from "./queries"; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 6d25944ef..b927a92b9 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request"; import { organizationKeys } from "../organization/queries"; import { User } from "../users/types"; -import { adminQueryKeys } from "./queries"; +import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { TCreateAdminUserDTO, TServerConfig } from "./types"; export const useCreateAdminUser = () => { @@ -43,3 +43,19 @@ export const useUpdateServerConfig = () => { } }); }; + +export const useAdminDeleteUser = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (userId: string) => { + await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}`); + + return {}; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [adminStandaloneKeys.getUsers] + }); + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index f64c8bfa9..91368fb9e 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -1,11 +1,17 @@ -import { useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TServerConfig } from "./types"; +import { User } from "../types"; +import { AdminGetUsersFilters, TServerConfig } from "./types"; + +export const adminStandaloneKeys = { + getUsers: "get-users" +}; export const adminQueryKeys = { - serverConfig: () => ["server-config"] as const + serverConfig: () => ["server-config"] as const, + getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const }; const fetchServerConfig = async () => { @@ -32,3 +38,24 @@ export const useGetServerConfig = ({ ...options, enabled: options?.enabled ?? true }); + +export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { + return useInfiniteQuery({ + queryKey: adminQueryKeys.getUsers(filters), + queryFn: async ({ pageParam }) => { + const { data } = await apiRequest.get<{ users: User[] }>( + "/api/v1/admin/user-management/users", + { + params: { + ...filters, + offset: pageParam + } + } + ); + + return data.users; + }, + getNextPageParam: (lastPage, pages) => + lastPage.length !== 0 ? pages.length * filters.limit : undefined + }); +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 8d5f59ede..bfa2e3e36 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,3 +1,13 @@ +export enum LoginMethod { + EMAIL = "email", + GOOGLE = "google", + GITHUB = "github", + GITLAB = "gitlab", + SAML = "saml", + LDAP = "ldap", + OIDC = "oidc" +} + export type TServerConfig = { initialized: boolean; allowSignUp: boolean; @@ -5,7 +15,11 @@ export type TServerConfig = { isMigrationModeOn?: boolean; trustSamlEmails: boolean; trustLdapEmails: boolean; + trustOidcEmails: boolean; isSecretScanningDisabled: boolean; + defaultAuthOrgSlug: string | null; + defaultAuthOrgId: string | null; + enabledLoginMethods: LoginMethod[]; }; export type TCreateAdminUserDTO = { @@ -23,3 +37,8 @@ export type TCreateAdminUserDTO = { verifier: string; salt: string; }; + +export type AdminGetUsersFilters = { + limit: number; + searchTerm: string; +}; diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 3b607e596..cdc973ed9 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -362,7 +362,6 @@ interface CreateWebhookEvent { webhookId: string; environment: string; secretPath: string; - webhookUrl: string; isDisabled: boolean; }; } @@ -373,7 +372,6 @@ interface UpdateWebhookStatusEvent { webhookId: string; environment: string; secretPath: string; - webhookUrl: string; isDisabled: boolean; }; } @@ -384,7 +382,6 @@ interface DeleteWebhookEvent { webhookId: string; environment: string; secretPath: string; - webhookUrl: string; isDisabled: boolean; }; } diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 64511253b..7cb5dbf42 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -81,6 +81,7 @@ export type TCreateCertificateDTO = { caId: string; friendlyName?: string; commonName: string; + altNames: string; // sans ttl: string; // string compatible with ms notBefore?: string; notAfter?: string; diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 2dd600187..d1ba46910 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -6,6 +6,7 @@ export type TCertificate = { status: CertStatus; friendlyName: string; commonName: string; + altNames: string; serialNumber: string; notBefore: string; notAfter: string; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index a9aab8318..b987e94cc 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -24,7 +24,8 @@ export enum DynamicSecretProviders { export enum SqlProviders { Postgres = "postgres", MySql = "mysql2", - Oracle = "oracledb" + Oracle = "oracledb", + MsSQL = "mssql" } export type TDynamicSecretProvider = diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index b87c3f13f..7e19ece33 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -17,6 +17,7 @@ export * from "./integrationAuth"; export * from "./integrations"; export * from "./keys"; export * from "./ldapConfig"; +export * from "./oidcConfig"; export * from "./organization"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index d800e5313..45ac90a84 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -802,6 +802,7 @@ export const useSaveIntegrationAccessToken = () => { refreshToken, accessId, accessToken, + awsAssumeIamRoleArn, url, namespace }: { @@ -810,6 +811,7 @@ export const useSaveIntegrationAccessToken = () => { refreshToken?: string; accessId?: string; accessToken?: string; + awsAssumeIamRoleArn?: string; url?: string; namespace?: string; }) => { @@ -821,6 +823,7 @@ export const useSaveIntegrationAccessToken = () => { refreshToken, accessId, accessToken, + awsAssumeIamRoleArn, url, namespace }); diff --git a/frontend/src/hooks/api/ldapConfig/mutations.tsx b/frontend/src/hooks/api/ldapConfig/mutations.tsx index a93286cdb..2014f52bd 100644 --- a/frontend/src/hooks/api/ldapConfig/mutations.tsx +++ b/frontend/src/hooks/api/ldapConfig/mutations.tsx @@ -13,6 +13,7 @@ export const useCreateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -24,6 +25,7 @@ export const useCreateLDAPConfig = () => { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; searchFilter: string; groupSearchBase: string; @@ -36,6 +38,7 @@ export const useCreateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -60,6 +63,7 @@ export const useUpdateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -71,6 +75,7 @@ export const useUpdateLDAPConfig = () => { url?: string; bindDN?: string; bindPass?: string; + uniqueUserAttribute?: string; searchBase?: string; searchFilter?: string; groupSearchBase?: string; @@ -83,6 +88,7 @@ export const useUpdateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, diff --git a/frontend/src/hooks/api/oidcConfig/index.tsx b/frontend/src/hooks/api/oidcConfig/index.tsx new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/frontend/src/hooks/api/oidcConfig/index.tsx @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/frontend/src/hooks/api/oidcConfig/mutations.tsx b/frontend/src/hooks/api/oidcConfig/mutations.tsx new file mode 100644 index 000000000..4a0d963cc --- /dev/null +++ b/frontend/src/hooks/api/oidcConfig/mutations.tsx @@ -0,0 +1,111 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { oidcConfigKeys } from "./queries"; + +export const useUpdateOIDCConfig = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + issuer, + authorizationEndpoint, + configurationType, + discoveryURL, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + allowedEmailDomains, + clientId, + clientSecret, + isActive, + orgSlug + }: { + allowedEmailDomains?: string; + issuer?: string; + authorizationEndpoint?: string; + discoveryURL?: string; + jwksUri?: string; + tokenEndpoint?: string; + userinfoEndpoint?: string; + clientId?: string; + clientSecret?: string; + isActive?: boolean; + configurationType?: string; + orgSlug: string; + }) => { + const { data } = await apiRequest.patch("/api/v1/sso/oidc/config", { + issuer, + allowedEmailDomains, + authorizationEndpoint, + discoveryURL, + configurationType, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + clientId, + orgSlug, + clientSecret, + isActive + }); + + return data; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug)); + } + }); +}; + +export const useCreateOIDCConfig = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + issuer, + configurationType, + discoveryURL, + authorizationEndpoint, + allowedEmailDomains, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + clientId, + clientSecret, + isActive, + orgSlug + }: { + issuer?: string; + configurationType: string; + discoveryURL?: string; + authorizationEndpoint?: string; + jwksUri?: string; + tokenEndpoint?: string; + userinfoEndpoint?: string; + clientId: string; + clientSecret: string; + isActive: boolean; + orgSlug: string; + allowedEmailDomains?: string; + }) => { + const { data } = await apiRequest.post("/api/v1/sso/oidc/config", { + issuer, + configurationType, + discoveryURL, + authorizationEndpoint, + allowedEmailDomains, + jwksUri, + tokenEndpoint, + userinfoEndpoint, + clientId, + clientSecret, + isActive, + orgSlug + }); + + return data; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(oidcConfigKeys.getOIDCConfig(dto.orgSlug)); + } + }); +}; diff --git a/frontend/src/hooks/api/oidcConfig/queries.tsx b/frontend/src/hooks/api/oidcConfig/queries.tsx new file mode 100644 index 000000000..08b38cbb8 --- /dev/null +++ b/frontend/src/hooks/api/oidcConfig/queries.tsx @@ -0,0 +1,23 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { OIDCConfigData } from "./types"; + +export const oidcConfigKeys = { + getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const +}; + +export const useGetOIDCConfig = (orgSlug: string) => { + return useQuery({ + queryKey: oidcConfigKeys.getOIDCConfig(orgSlug), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/sso/oidc/config?orgSlug=${orgSlug}` + ); + + return data; + }, + enabled: true + }); +}; diff --git a/frontend/src/hooks/api/oidcConfig/types.ts b/frontend/src/hooks/api/oidcConfig/types.ts new file mode 100644 index 000000000..1b8ede5e3 --- /dev/null +++ b/frontend/src/hooks/api/oidcConfig/types.ts @@ -0,0 +1,15 @@ +export type OIDCConfigData = { + id: string; + issuer: string; + authorizationEndpoint: string; + configurationType: string; + discoveryURL: string; + jwksUri: string; + tokenEndpoint: string; + userinfoEndpoint: string; + isActive: boolean; + orgId: string; + clientId: string; + clientSecret: string; + allowedEmailDomains?: string; +}; diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index e0a8df95d..991111ef9 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -9,12 +9,12 @@ export const useCreateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateSecretPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => { + mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, workspaceId, approvals, - approvers, + approverUserIds, secretPath, name }); @@ -30,10 +30,10 @@ export const useUpdateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, secretPath, name }) => { + mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, - approvers, + approverUserIds, secretPath, name }); diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 38e8d7ee8..f3b8639f7 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -7,8 +7,8 @@ export type TSecretApprovalPolicy = { envId: string; environment: WorkspaceEnv; secretPath?: string; - approvers: string[]; approvals: number; + userApprovers: { userId: string }[]; }; export type TGetSecretApprovalPoliciesDTO = { @@ -26,14 +26,14 @@ export type TCreateSecretPolicyDTO = { name?: string; environment: string; secretPath?: string | null; - approvers?: string[]; + approverUserIds?: string[]; approvals?: number; }; export type TUpdateSecretPolicyDTO = { id: string; name?: string; - approvers?: string[]; + approverUserIds?: string[]; secretPath?: string | null; approvals?: number; // for invalidating list diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index 8c2ba6963..f039cc815 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -47,10 +47,14 @@ export type TSecretApprovalRequest = { isReplicated?: boolean; slug: string; createdAt: string; - committerId: string; + committerUserId: string; reviewers: { - member: string; + userId: string; status: ApprovalStatus; + email: string; + firstName: string; + lastName: string; + username: string; }[]; workspace: string; environment: string; @@ -58,8 +62,30 @@ export type TSecretApprovalRequest = { secretPath: string; hasMerged: boolean; status: "open" | "close"; - policy: TSecretApprovalPolicy; - statusChangeBy: string; + policy: Omit & { + approvers: { + userId: string; + email: string; + firstName: string; + lastName: string; + username: string; + }[]; + }; + statusChangedByUserId: string; + statusChangedByUser?: { + userId: string; + email: string; + firstName: string; + lastName: string; + username: string; + }; + committerUser: { + userId: string; + email: string; + firstName: string; + lastName: string; + username: string; + }; conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>; commits: ({ // if there is no secret means it was creation diff --git a/frontend/src/hooks/api/secretImports/queries.tsx b/frontend/src/hooks/api/secretImports/queries.tsx index 701a20543..a7c510662 100644 --- a/frontend/src/hooks/api/secretImports/queries.tsx +++ b/frontend/src/hooks/api/secretImports/queries.tsx @@ -279,7 +279,28 @@ export const useGetImportedSecretsAllEnvs = ({ [(secretImports || []).map((response) => response.data)] ); - return { secretImports, isImportedSecretPresentInEnv }; + const getImportedSecretByKey = useCallback( + (envSlug: string, secretName: string) => { + const selectedEnvIndex = environments.indexOf(envSlug); + + if (selectedEnvIndex !== -1) { + const secret = secretImports?.[selectedEnvIndex]?.data?.find(({ secrets }) => + secrets.find((s) => s.key === secretName) + ); + + if (!secret) return undefined; + + return { + secret: secret?.secrets.find((s) => s.key === secretName), + environmentInfo: secret?.environmentInfo + }; + } + return undefined; + }, + [(secretImports || []).map((response) => response.data)] + ); + + return { secretImports, isImportedSecretPresentInEnv, getImportedSecretByKey }; }; export const useGetImportedFoldersByEnv = ({ diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index ca1ec76fd..cd81152a1 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -7,7 +7,7 @@ import { } from "@app/components/utilities/cryptography/crypto"; import { apiRequest } from "@app/config/request"; -import { DecryptedSecret } from "../secrets/types"; +import { DecryptedSecret, SecretType } from "../secrets/types"; import { TGetSecretSnapshotsDTO, TSecretRollbackDTO, @@ -112,7 +112,7 @@ export const useGetSnapshotSecrets = ({ decryptFileKey, snapshotId }: TSnapshotD version: encSecret.version }; - if (encSecret.type === "personal") { + if (encSecret.type === SecretType.Personal) { personalSecrets[decryptedSecret.key] = { id: encSecret.secretId, value: secretValue }; } else { sharedSecrets.push(decryptedSecret); diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 28999389e..382d01785 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -14,6 +14,7 @@ import { EncryptedSecret, EncryptedSecretVersion, GetSecretVersionsDTO, + SecretType, TGetProjectSecretsAllEnvDTO, TGetProjectSecretsDTO, TGetProjectSecretsKey @@ -77,7 +78,7 @@ export const decryptSecrets = ( skipMultilineEncoding: encSecret.skipMultilineEncoding }; - if (encSecret.type === "personal") { + if (encSecret.type === SecretType.Personal) { personalSecrets[decryptedSecret.key] = { id: encSecret.id, value: secretValue diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index f36872e43..378405c96 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -1,11 +1,16 @@ import type { UserWsKeyPair } from "../keys/types"; import type { WsTag } from "../tags/types"; +export enum SecretType { + Shared = "shared", + Personal = "personal" +} + export type EncryptedSecret = { id: string; version: number; workspace: string; - type: "shared" | "personal"; + type: SecretType; environment: string; secretKeyCiphertext: string; secretKeyIV: string; @@ -49,7 +54,7 @@ export type EncryptedSecretVersion = { secretId: string; version: number; workspace: string; - type: string; + type: SecretType; isDeleted: boolean; envId: string; secretKeyCiphertext: string; @@ -101,14 +106,14 @@ export type TCreateSecretsV3DTO = { secretPath: string; workspaceId: string; environment: string; - type: string; + type: SecretType; }; export type TUpdateSecretsV3DTO = { latestFileKey: UserWsKeyPair; workspaceId: string; environment: string; - type: string; + type: SecretType; secretPath: string; skipMultilineEncoding?: boolean; newSecretName?: string; @@ -124,7 +129,7 @@ export type TUpdateSecretsV3DTO = { export type TDeleteSecretsV3DTO = { workspaceId: string; environment: string; - type: "shared" | "personal"; + type: SecretType; secretPath: string; secretName: string; secretId?: string; @@ -140,7 +145,7 @@ export type TCreateSecretBatchDTO = { secretValue: string; secretComment: string; skipMultilineEncoding?: boolean; - type: "shared" | "personal"; + type: SecretType; metadata?: { source?: string; }; @@ -153,7 +158,7 @@ export type TUpdateSecretBatchDTO = { secretPath: string; latestFileKey: UserWsKeyPair; secrets: Array<{ - type: "shared" | "personal"; + type: SecretType; secretName: string; skipMultilineEncoding?: boolean; secretValue: string; @@ -168,14 +173,14 @@ export type TDeleteSecretBatchDTO = { secretPath: string; secrets: Array<{ secretName: string; - type: "shared" | "personal"; + type: SecretType; }>; }; export type CreateSecretDTO = { workspaceId: string; environment: string; - type: "shared" | "personal"; + type: SecretType; secretKey: string; secretKeyCiphertext: string; secretKeyIV: string; diff --git a/frontend/src/hooks/api/serverDetails/types.ts b/frontend/src/hooks/api/serverDetails/types.ts index 911526404..3e22c2684 100644 --- a/frontend/src/hooks/api/serverDetails/types.ts +++ b/frontend/src/hooks/api/serverDetails/types.ts @@ -4,5 +4,5 @@ export type ServerStatus = { emailConfigured: boolean; secretScanningConfigured: boolean; redisConfigured: boolean; - samlDefaultOrgSlug: boolean + samlDefaultOrgSlug: string; }; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 66959ad1b..89635a953 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -2,6 +2,8 @@ export type SubscriptionPlan = { id: string; membersUsed: number; memberLimit: number; + identitiesUsed: number; + identityLimit: number; auditLogs: boolean; dynamicSecret: boolean; auditLogsRetentionDays: number; @@ -21,6 +23,7 @@ export type SubscriptionPlan = { workspacesUsed: number; environmentLimit: number; samlSSO: boolean; + oidcSSO: boolean; scim: boolean; ldap: boolean; groups: boolean; diff --git a/frontend/src/hooks/api/userEngagement/index.ts b/frontend/src/hooks/api/userEngagement/index.ts new file mode 100644 index 000000000..5a4c29fa8 --- /dev/null +++ b/frontend/src/hooks/api/userEngagement/index.ts @@ -0,0 +1 @@ +export { useCreateUserWish } from "./mutations"; diff --git a/frontend/src/hooks/api/userEngagement/mutations.tsx b/frontend/src/hooks/api/userEngagement/mutations.tsx new file mode 100644 index 000000000..d876e65c8 --- /dev/null +++ b/frontend/src/hooks/api/userEngagement/mutations.tsx @@ -0,0 +1,14 @@ +import { useMutation } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TCreateUserWishDto } from "./types"; + +export const useCreateUserWish = () => { + return useMutation<{}, {}, TCreateUserWishDto>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/user-engagement/me/wish", dto); + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/userEngagement/types.ts b/frontend/src/hooks/api/userEngagement/types.ts new file mode 100644 index 000000000..ad94d03d2 --- /dev/null +++ b/frontend/src/hooks/api/userEngagement/types.ts @@ -0,0 +1,3 @@ +export type TCreateUserWishDto = { + text: string; +}; diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index a8ad89f4c..521c36468 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -9,8 +9,8 @@ export { useAddUserToOrg, useCreateAPIKey, useDeleteAPIKey, + useDeleteMe, useDeleteOrgMembership, - useDeleteUser, useGetMyAPIKeys, useGetMyAPIKeysV2, useGetMyIp, diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index 20e986aab..26e932ac6 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -7,6 +7,7 @@ import { import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; +import { userKeys } from "./queries"; import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types"; export const useAddUserToWsE2EE = () => { @@ -88,3 +89,26 @@ export const useVerifyEmailVerificationCode = () => { } }); }; + +export const useUpdateUserProjectFavorites = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + orgId, + projectFavorites + }: { + orgId: string; + projectFavorites: string[]; + }) => { + await apiRequest.put("/api/v1/user/me/project-favorites", { + orgId, + projectFavorites + }); + + return {}; + }, + onSuccess: (_, { orgId }) => { + queryClient.invalidateQueries(userKeys.userProjectFavorites(orgId)); + } + }); +}; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index fa0b932ea..5ca47fc63 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -22,11 +22,14 @@ export const userKeys = { getUser: ["user"] as const, getPrivateKey: ["user"] as const, userAction: ["user-action"] as const, + userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const, getOrgUsers: (orgId: string) => [{ orgId }, "user"], myIp: ["ip"] as const, myAPIKeys: ["api-keys"] as const, myAPIKeysV2: ["api-keys-v2"] as const, mySessions: ["sessions"] as const, + listUsers: ["user-list"] as const, + myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const }; @@ -38,7 +41,7 @@ export const fetchUserDetails = async () => { export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails); -export const useDeleteUser = () => { +export const useDeleteMe = () => { const queryClient = useQueryClient(); return useMutation({ @@ -74,6 +77,14 @@ export const fetchUserAction = async (action: string) => { return data.userAction || ""; }; +export const fetchUserProjectFavorites = async (orgId: string) => { + const { data } = await apiRequest.get<{ projectFavorites: string[] }>( + `/api/v1/user/me/project-favorites?orgId=${orgId}` + ); + + return data.projectFavorites; +}; + export const useRenameUser = () => { const queryClient = useQueryClient(); @@ -122,6 +133,12 @@ export const fetchOrgUsers = async (orgId: string) => { return data.users; }; +export const useGetUserProjectFavorites = (orgId: string) => + useQuery({ + queryKey: userKeys.userProjectFavorites(orgId), + queryFn: () => fetchUserProjectFavorites(orgId) + }); + export const useGetOrgUsers = (orgId: string) => useQuery({ queryKey: userKeys.getOrgUsers(orgId), diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 649af434c..43e408571 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -29,7 +29,8 @@ export type User = { export enum UserAliasType { LDAP = "ldap", - SAML = "saml" + SAML = "saml", + OIDC = "oidc" } export type UserEnc = { diff --git a/frontend/src/hooks/api/webhooks/types.ts b/frontend/src/hooks/api/webhooks/types.ts index 447ed4fc5..86183bf1b 100644 --- a/frontend/src/hooks/api/webhooks/types.ts +++ b/frontend/src/hooks/api/webhooks/types.ts @@ -1,5 +1,11 @@ +export enum WebhookType { + GENERAL = "general", + SLACK = "slack" +} + export type TWebhook = { id: string; + type: WebhookType; projectId: string; environment: { slug: string; @@ -22,6 +28,7 @@ export type TCreateWebhookDto = { webhookUrl: string; webhookSecretKey?: string; secretPath: string; + type: WebhookType; }; export type TUpdateWebhookDto = { diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 7f94bb498..202480de8 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -22,6 +22,7 @@ import { ToggleAutoCapitalizationDTO, TUpdateWorkspaceIdentityRoleDTO, TUpdateWorkspaceUserRoleDTO, + UpdateAuditLogsRetentionDTO, UpdateEnvironmentDTO, UpdatePitVersionLimitDTO, Workspace @@ -284,6 +285,21 @@ export const useUpdateWorkspaceVersionLimit = () => { }); }; +export const useUpdateWorkspaceAuditLogsRetention = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, UpdateAuditLogsRetentionDTO>({ + mutationFn: ({ projectSlug, auditLogsRetentionDays }) => { + return apiRequest.put(`/api/v1/workspace/${projectSlug}/audit-logs-retention`, { + auditLogsRetentionDays + }); + }, + onSuccess: () => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + } + }); +}; + export const useDeleteWorkspace = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 5a90cf2b1..53994e88c 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -18,6 +18,7 @@ export type Workspace = { autoCapitalization: boolean; environments: WorkspaceEnv[]; pitVersionLimit: number; + auditLogsRetentionDays: number; slug: string; }; @@ -51,6 +52,7 @@ export type CreateWorkspaceDTO = { export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; +export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number }; export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean }; export type DeleteWorkspaceDTO = { workspaceID: string }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 72351df88..dc0ef370e 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -8,10 +8,10 @@ import { useEffect, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; +import { faStar } from "@fortawesome/free-regular-svg-icons"; import { faAngleDown, faArrowLeft, @@ -23,7 +23,8 @@ import { faInfo, faMobile, faPlus, - faQuestion + faQuestion, + faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; @@ -67,14 +68,17 @@ import { useGetAccessRequestsCount, useGetOrgTrialUrl, useGetSecretApprovalRequestCount, - useGetUserAction, useLogoutUser, - useRegisterUserAction, useSelectOrganization } from "@app/hooks/api"; +import { Workspace } from "@app/hooks/api/types"; +import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; +import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; import { CreateOrgModal } from "@app/views/Org/components"; +import { WishForm } from "./components/WishForm/WishForm"; + interface LayoutProps { children: React.ReactNode; } @@ -122,11 +126,24 @@ export const AppLayout = ({ children }: LayoutProps) => { const { workspaces, currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); + const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); + const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); + + const workspacesWithFaveProp = useMemo( + () => + workspaces + .map((w): Workspace & { isFavorite: boolean } => ({ + ...w, + isFavorite: Boolean(projectFavorites?.includes(w.id)) + })) + .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)), + [workspaces, projectFavorites] + ); + const { user } = useUser(); const { subscription } = useSubscription(); const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; - const { data: updateClosed } = useGetUserAction("december_update_closed"); const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); @@ -160,13 +177,8 @@ export const AppLayout = ({ children }: LayoutProps) => { const { t } = useTranslation(); - const registerUserAction = useRegisterUserAction(); const { mutateAsync: selectOrganization } = useSelectOrganization(); - const closeUpdate = async () => { - await registerUserAction.mutateAsync("december_update_closed"); - }; - const logout = useLogoutUser(); const logOutUser = async () => { try { @@ -271,6 +283,38 @@ export const AppLayout = ({ children }: LayoutProps) => { } }; + const addProjectToFavorites = async (projectId: string) => { + try { + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []), projectId] + }); + } + } catch (err) { + createNotification({ + text: "Failed to add project to favorites.", + type: "error" + }); + } + }; + + const removeProjectFromFavorites = async (projectId: string) => { + try { + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] + }); + } + } catch (err) { + createNotification({ + text: "Failed to remove project from favorites.", + type: "error" + }); + } + }; + return ( <>
@@ -451,19 +495,47 @@ export const AppLayout = ({ children }: LayoutProps) => { dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700" >
- {workspaces + {workspacesWithFaveProp .filter((ws) => ws.orgId === currentOrg?.id) - .map(({ id, name }) => ( - ( +
- {name} - +
+ + {name} + +
+
+ {isFavorite ? ( + { + e.stopPropagation(); + removeProjectFromFavorites(id); + }} + /> + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(id); + }} + /> + )} +
+
))}

@@ -686,49 +758,8 @@ export const AppLayout = ({ children }: LayoutProps) => { : "mb-4" } flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`} > - {/*
-
-
-
*/} -
-
- Infisical December update -
-
- Infisical Agent, new SDKs, Machine Identities, and more! -
-
- kubernetes image -
-
- - - Learn More{" "} - - -
-
+ {(window.location.origin.includes("https://app.infisical.com") || + window.location.origin.includes("https://gamma.infisical.com")) && } {router.asPath.includes("org") && (
null} diff --git a/frontend/src/layouts/AppLayout/components/WishForm/WishForm.tsx b/frontend/src/layouts/AppLayout/components/WishForm/WishForm.tsx new file mode 100644 index 000000000..bf87d0672 --- /dev/null +++ b/frontend/src/layouts/AppLayout/components/WishForm/WishForm.tsx @@ -0,0 +1,109 @@ +import { useForm } from "react-hook-form"; +import { faRocketchat } from "@fortawesome/free-brands-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Popover, + PopoverContent, + PopoverTrigger, + TextArea +} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { useCreateUserWish } from "@app/hooks/api/userEngagement"; + +const formSchema = z.object({ + text: z.string().trim().min(1) +}); + +type TFormData = z.infer; + +export const WishForm = () => { + const { + handleSubmit, + register, + reset, + formState: { isSubmitting, errors } + } = useForm({ + resolver: zodResolver(formSchema) + }); + const { mutateAsync } = useCreateUserWish(); + const [isOpen, setIsOpen] = useToggle(false); + + const createWish = async (data: TFormData) => { + try { + await mutateAsync({ + text: data.text + }); + + createNotification({ + text: "Your wish has been sent to the Infisical team!", + type: "success" + }); + + setIsOpen.off(); + } catch (err) { + createNotification({ + text: "An error occured while sending your wish to the Infisical team.", + type: "error" + }); + } + }; + + return ( + { + setIsOpen.toggle(); + reset(); + }} + open={isOpen} + > + +
+ + Make a wish +
+
+ +
+ +