diff --git a/.github/workflows/check-fe-ts-and-lint.yml b/.github/workflows/check-fe-ts-and-lint.yml index 9b4d363cff..c9b38d068c 100644 --- a/.github/workflows/check-fe-ts-and-lint.yml +++ b/.github/workflows/check-fe-ts-and-lint.yml @@ -30,6 +30,6 @@ jobs: - name: 🏗️ Run Type check run: npm run type:check working-directory: frontend - - name: 🏗️ Run Link check - run: npm run lint:fix + - name: 🏗️ Run Lint check + run: npm run lint working-directory: frontend diff --git a/.github/workflows/helm-release-infisical-core.yml b/.github/workflows/helm-release-infisical-core.yml index ebce7cbe0e..6c317cc27c 100644 --- a/.github/workflows/helm-release-infisical-core.yml +++ b/.github/workflows/helm-release-infisical-core.yml @@ -30,6 +30,8 @@ jobs: - name: Set up chart-testing uses: helm/chart-testing-action@v2.7.0 + with: + yamale_version: "6.0.0" - name: Run chart-testing (lint) run: ct lint --config ct.yaml --charts helm-charts/infisical-standalone-postgres diff --git a/.github/workflows/nightly-tag-generation.yml b/.github/workflows/nightly-tag-generation.yml index b2704f96c9..ff15b61d8f 100644 --- a/.github/workflows/nightly-tag-generation.yml +++ b/.github/workflows/nightly-tag-generation.yml @@ -1,8 +1,6 @@ name: Generate Nightly Tag on: - schedule: - - cron: '0 0 * * *' # Run daily at midnight UTC workflow_dispatch: # Allow manual triggering for testing permissions: diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index 486ff12b26..9dc767e310 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -65,6 +65,15 @@ jobs: INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} DD_GIT_REPOSITORY_URL=${{ github.server_url }}/${{ github.repository }} DD_GIT_COMMIT_SHA=${{ github.sha }} + - name: Snyk to check Docker image for vulnerabilities + continue-on-error: true + uses: snyk/actions/docker@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + image: infisical/infisical:${{ steps.extract_version.outputs.version }} + command: monitor + args: --file=Dockerfile.standalone-infisical --project-name="infisical-core-docker-image" infisical-fips-standalone: name: Build infisical standalone image postgres @@ -141,4 +150,4 @@ jobs: echo "Successfully created tag $TAG_NAME" fi env: - GH_TOKEN: ${{ secrets.OMNIBUS_RELEASE_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.OMNIBUS_RELEASE_TOKEN }} diff --git a/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml b/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml index 50cb9ecb07..d48562fc68 100644 --- a/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml +++ b/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml @@ -33,6 +33,8 @@ jobs: - name: Set up chart-testing uses: helm/chart-testing-action@v2.7.0 + with: + yamale_version: "6.0.0" - name: Run chart-testing (lint) run: ct lint --config ct.yaml --charts helm-charts/infisical-standalone-postgres diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index 974ce4a429..1c03f752ab 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -3,7 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id ARG CAPTCHA_SITE_KEY=captcha-site-key -FROM node:20-slim AS base +FROM node:20.19.5-trixie-slim AS base + +# Fixes NPM vulnerability: https://security.snyk.io/vuln/SNYK-JS-CROSSSPAWN-8303230 +RUN npm install -g npm@11 FROM base AS frontend-dependencies WORKDIR /app @@ -155,7 +158,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.41.89 \ + && apt-get update && apt-get install -y infisical=0.42.6 \ && rm -rf /var/lib/apt/lists/* RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 9ca5e1dea9..bea94d6b6b 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -3,7 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id ARG CAPTCHA_SITE_KEY=captcha-site-key -FROM node:20-slim AS base +FROM node:20.19.5-trixie-slim AS base + +# Fixes NPM vulnerability: https://security.snyk.io/vuln/SNYK-JS-CROSSSPAWN-8303230 +RUN npm install -g npm@11 FROM base AS frontend-dependencies @@ -139,7 +142,7 @@ RUN apt-get update && apt-get install -y \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.41.89 \ + && apt-get update && apt-get install -y infisical=0.42.6 \ && rm -rf /var/lib/apt/lists/* WORKDIR / diff --git a/backend/Dockerfile b/backend/Dockerfile index bca974f26d..fa3d0e509c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM node:20-slim AS build +FROM node:20.19.5-trixie-slim AS build WORKDIR /app @@ -26,7 +26,7 @@ COPY . . RUN npm run build # Production stage -FROM node:20-slim +FROM node:20.19.5-trixie-slim WORKDIR /app ENV npm_config_cache /home/node/.npm diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index de56487972..5e17cf2bbc 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM node:20-slim +FROM node:20.19.5-trixie-slim # ? Setup a test SoftHSM module. In production a real HSM is used. diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index b954ccd50e..db51079857 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -1,4 +1,4 @@ -FROM node:20-slim +FROM node:20.19.5-trixie-slim # ? Setup a test SoftHSM module. In production a real HSM is used. diff --git a/backend/package-lock.json b/backend/package-lock.json index db4e9e7dd0..adda83043b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,7 @@ "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", - "@elastic/elasticsearch": "^8.15.0", + "@elastic/elasticsearch": "^9.1.1", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", @@ -63,7 +63,7 @@ "ajv": "^8.12.0", "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "axios-ntlm": "^1.4.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", @@ -74,7 +74,7 @@ "cron": "^3.1.7", "dd-trace": "^5.40.0", "dotenv": "^16.4.1", - "fastify": "^4.28.1", + "fastify": "^4.29.1", "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", @@ -141,6 +141,7 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.24.7", + "@react-email/preview-server": "^4.3.0", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -176,7 +177,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "4.0.7", + "react-email": "^4.3.0", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -196,6 +197,19 @@ "node": ">=0.10.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -5150,30 +5164,32 @@ } }, "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==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, + "license": "MIT", "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==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, + "license": "MIT", "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", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -5221,16 +5237,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -5238,10 +5254,11 @@ } }, "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==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -5286,14 +5303,15 @@ } }, "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==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -5447,6 +5465,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "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", @@ -5473,29 +5501,29 @@ } }, "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==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "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==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, + "license": "MIT", "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" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -5618,10 +5646,11 @@ } }, "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==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -5642,26 +5671,27 @@ } }, "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==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -6998,14 +7028,14 @@ } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -7013,19 +7043,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" @@ -7048,15 +7078,6 @@ } } }, - "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", @@ -7064,9 +7085,9 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7329,12 +7350,13 @@ "integrity": "sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg==" }, "node_modules/@elastic/elasticsearch": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz", - "integrity": "sha512-mG90EMdTDoT6GFSdqpUAhWK9LGuiJo6tOWqs0Usd/t15mPQDj7ZqHXfCBqNkASZpwPZpbAYVjd57S6nbUBINCg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-9.1.1.tgz", + "integrity": "sha512-s/JZtHZjtbAYC2gdSzm4LLOSReR724e7cf7ZauIAZlGvAyMgZPZCJpq7xHazSy4rZZhule4ubMs4vepBgWvKQA==", "license": "Apache-2.0", "dependencies": { - "@elastic/transport": "^8.7.0", + "@elastic/transport": "^9.0.1", + "apache-arrow": "18.x - 20.x", "tslib": "^2.4.0" }, "engines": { @@ -7342,30 +7364,55 @@ } }, "node_modules/@elastic/transport": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.7.1.tgz", - "integrity": "sha512-2eeMVkz57Ayxv+UAZkIKzzrUu7nm96jr3+N3kLfbBqALYe2jwDpLr9pR0jc/x9HyJKAM909YGaNlHFDZeb0+Mw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-9.2.0.tgz", + "integrity": "sha512-2HpxEX9eQE/viokiKHqRa1n3RaFqNKoOU5gc7AOJ4ahG9xZbim+Z3OdBwshW9aKuFeIn1WPtZxSrfghZ0UJFtg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "1.x", - "debug": "^4.3.4", - "hpagent": "^1.0.0", + "@opentelemetry/core": "2.x", + "debug": "^4.4.1", + "hpagent": "^1.2.0", "ms": "^2.1.3", - "secure-json-parse": "^2.4.0", - "tslib": "^2.4.0", - "undici": "^6.12.0" + "secure-json-parse": "^4.0.0", + "tslib": "^2.8.1", + "undici": "^7.16.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/@elastic/transport/node_modules/@opentelemetry/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", + "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@elastic/transport/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", + "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" } }, "node_modules/@elastic/transport/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -7376,16 +7423,26 @@ } } }, - "node_modules/@elastic/transport/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" + "node_modules/@elastic/transport/node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "dev": true, "license": "MIT", "optional": true, @@ -7732,6 +7789,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", @@ -8234,6 +8308,48 @@ "p-limit": "^3.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@gitbeaker/core": { "version": "42.5.0", "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-42.5.0.tgz", @@ -8535,10 +8651,20 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", "cpu": [ "arm64" ], @@ -8555,13 +8681,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", "cpu": [ "x64" ], @@ -8578,13 +8704,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.3" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", "cpu": [ "arm64" ], @@ -8599,9 +8725,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", "cpu": [ "x64" ], @@ -8616,9 +8742,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", "cpu": [ "arm" ], @@ -8633,9 +8759,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", "cpu": [ "arm64" ], @@ -8649,10 +8775,27 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", "cpu": [ "s390x" ], @@ -8667,9 +8810,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", "cpu": [ "x64" ], @@ -8684,9 +8827,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", "cpu": [ "arm64" ], @@ -8701,9 +8844,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", "cpu": [ "x64" ], @@ -8718,9 +8861,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", "cpu": [ "arm" ], @@ -8737,13 +8880,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.2.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", "cpu": [ "arm64" ], @@ -8760,13 +8903,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", "cpu": [ "s390x" ], @@ -8783,13 +8949,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", "cpu": [ "x64" ], @@ -8806,13 +8972,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", "cpu": [ "arm64" ], @@ -8829,13 +8995,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", "cpu": [ "x64" ], @@ -8852,13 +9018,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", "cpu": [ "wasm32" ], @@ -8866,7 +9032,7 @@ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.5.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -8875,10 +9041,30 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", "cpu": [ "ia32" ], @@ -8896,9 +9082,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", "cpu": [ "x64" ], @@ -9026,6 +9212,29 @@ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -9084,17 +9293,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { @@ -9116,20 +9322,34 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -9232,6 +9452,26 @@ "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" }, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.3.tgz", + "integrity": "sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lottiefiles/dotlottie-web": "0.42.0" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.42.0.tgz", + "integrity": "sha512-Zr2LCaOAoPCsdAQgeLyCSiQ1+xrAJtRCyuEYDj0qR5heUwpc+Pxbb88JyTVumcXFfKOBMOMmrlsTScLz2mrvQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -9453,16 +9693,16 @@ ] }, "node_modules/@next/env": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", - "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", + "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", "dev": true, "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", - "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", + "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", "cpu": [ "arm64" ], @@ -9477,9 +9717,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", - "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", + "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", "cpu": [ "x64" ], @@ -9494,9 +9734,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", - "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", + "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", "cpu": [ "arm64" ], @@ -9511,9 +9751,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", - "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", + "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", "cpu": [ "arm64" ], @@ -9528,9 +9768,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", - "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", + "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", "cpu": [ "x64" ], @@ -9545,9 +9785,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", - "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", + "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", "cpu": [ "x64" ], @@ -9562,9 +9802,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", - "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", + "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", "cpu": [ "arm64" ], @@ -9579,9 +9819,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", - "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", + "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", "cpu": [ "x64" ], @@ -11100,6 +11340,787 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@react-email/body": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz", @@ -11314,6 +12335,849 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@react-email/preview-server": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-4.3.0.tgz", + "integrity": "sha512-cUaSrxezCzdg2hF6PzIxVrtagLdw3z3ovHeB3y2RDkmDZpp7EeIoNyJm22Ch2S0uAqTZNAgqu67aroLn3mFC1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.26.10", + "@babel/parser": "7.27.0", + "@babel/traverse": "7.27.0", + "@lottiefiles/dotlottie-react": "0.13.3", + "@radix-ui/colors": "3.0.0", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@types/node": "22.14.1", + "@types/normalize-path": "3.0.2", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@types/webpack": "5.28.5", + "autoprefixer": "10.4.21", + "clsx": "2.1.1", + "esbuild": "0.25.10", + "framer-motion": "12.23.22", + "json5": "2.2.3", + "log-symbols": "4.1.0", + "module-punycode": "npm:punycode@2.3.1", + "next": "15.5.2", + "node-html-parser": "7.0.1", + "ora": "5.4.1", + "pretty-bytes": "6.1.1", + "prism-react-renderer": "2.4.1", + "react": "19.0.0", + "react-dom": "19.0.0", + "sharp": "0.34.4", + "socket.io-client": "4.8.1", + "sonner": "2.0.3", + "source-map-js": "1.2.1", + "spamc": "0.0.5", + "stacktrace-parser": "0.1.11", + "tailwind-merge": "3.2.0", + "tailwindcss": "3.4.0", + "use-debounce": "10.0.4", + "zod": "3.24.3" + } + }, + "node_modules/@react-email/preview-server/node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-email/preview-server/node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/@react-email/preview-server/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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "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/@react-email/preview-server/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@react-email/preview-server/node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-email/preview-server/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-email/preview-server/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-email/preview-server/node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@react-email/preview-server/node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@react-email/preview-server/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-email/preview-server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@react-email/render": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", @@ -12996,18 +14860,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -13088,6 +14944,18 @@ "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "license": "MIT" }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "license": "MIT" + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -13115,6 +14983,28 @@ "@types/ms": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -13284,6 +15174,13 @@ "@types/node": "*" } }, + "node_modules/@types/normalize-path": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.2.tgz", + "integrity": "sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/oauth": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.4.tgz", @@ -13427,6 +15324,13 @@ "pkcs11js": "*" } }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -13454,6 +15358,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, "node_modules/@types/readable-stream": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", @@ -13599,6 +15513,18 @@ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", @@ -14073,6 +15999,167 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, "node_modules/@xmldom/is-dom-node": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz", @@ -14091,6 +16178,20 @@ "node": ">=10.0.0" } }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@yao-pkg/pkg": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.12.0.tgz", @@ -14365,9 +16466,10 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -14383,6 +16485,19 @@ "acorn": "^8" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -14490,6 +16605,19 @@ } } }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -14546,6 +16674,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/apache-arrow": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-20.0.0.tgz", + "integrity": "sha512-JUeK0jFRUd7rbmrhhzR3O2KXjLaZ4YYYFOptyUfxOsMIoZCPi6bZR58gVi/xi3HTBMPseXm9PXyQ2V916930pA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/command-line-args": "^5.2.3", + "@types/command-line-usage": "^5.0.4", + "@types/node": "^20.13.0", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^25.1.24", + "json-bignum": "^0.0.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.js" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -14665,6 +16813,28 @@ "node": ">=0.8.0" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -14905,10 +17075,52 @@ "node": ">=8.0.0" } }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -15014,9 +17226,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -15161,6 +17373,16 @@ "node": ">=6.0.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz", + "integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -15311,6 +17533,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/botbuilder": { "version": "4.23.2", "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.2.tgz", @@ -15590,9 +17819,9 @@ ] }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -15608,11 +17837,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -15662,6 +17893,13 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bullmq": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.4.2.tgz", @@ -15707,18 +17945,6 @@ "esbuild": ">=0.17" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -15855,16 +18081,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -15885,6 +18110,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -15902,10 +18143,20 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "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==", + "version": "1.0.30001748", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", + "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", "dev": true, "funding": [ { @@ -15920,7 +18171,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/cassandra-driver": { "version": "4.7.2", @@ -15965,6 +18217,58 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/chalk-template/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk-template/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -16012,13 +18316,37 @@ "node": ">=10" } }, - "node_modules/cipher-base": { + "node_modules/chrome-trace-event": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.5.tgz", + "integrity": "sha512-xq7ICKB4TMHUx7Tz1L9O2SGKOhYMOTR32oir45Bq28/AQTpHogKgHcoYFSdRbMtddl+ozNXfXY9jWcgYKmde0w==", + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" } }, "node_modules/cjs-module-lexer": { @@ -16145,6 +18473,16 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -16239,6 +18577,44 @@ "node": ">= 0.8" } }, + "node_modules/command-line-args": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "find-replace": "^5.0.2", + "lodash.camelcase": "^4.3.0", + "typical": "^7.2.0" + }, + "engines": { + "node": ">=12.20" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/command-line-usage": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -16252,6 +18628,13 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -16269,6 +18652,16 @@ "express-session": ">=1" } }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -16414,6 +18807,49 @@ "resolved": "https://registry.npmjs.org/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz", "integrity": "sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==" }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", @@ -16816,9 +19252,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -16832,12 +19268,26 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/dev-null": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==", "license": "MIT" }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -16868,6 +19318,13 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -17028,10 +19485,11 @@ "license": "MIT" }, "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 + "version": "1.5.232", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz", + "integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "10.3.0", @@ -17092,6 +19550,60 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -17143,10 +19655,11 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -17259,6 +19772,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -17354,9 +19874,10 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -18080,6 +20601,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -18225,9 +20753,9 @@ } }, "node_modules/fastify": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.28.1.tgz", - "integrity": "sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==", + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", "funding": [ { "type": "github", @@ -18443,6 +20971,23 @@ "node": ">=14" } }, + "node_modules/find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -18508,6 +21053,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -18540,19 +21091,27 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -18602,6 +21161,48 @@ "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.23.22", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", + "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.21", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -18920,6 +21521,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -18952,6 +21566,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -19062,6 +21686,13 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -19456,6 +22087,16 @@ "node": ">=0.10.0" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/helmet": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", @@ -20304,11 +22945,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -20354,8 +22996,7 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/isexe": { "version": "2.0.0", @@ -20421,6 +23062,57 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -20600,6 +23292,14 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-bignum": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -20611,6 +23311,13 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -20846,6 +23553,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/knex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/knex/-/knex-3.0.1.tgz", @@ -21143,6 +23860,16 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -21568,6 +24295,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -21797,6 +24537,17 @@ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, + "node_modules/module-punycode": { + "name": "punycode", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -21903,6 +24654,23 @@ "node": ">=16" } }, + "node_modules/motion-dom": { + "version": "12.23.21", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz", + "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -22109,16 +24877,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/next": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", - "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", + "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", "dev": true, "license": "MIT", "dependencies": { - "@next/env": "15.2.4", - "@swc/counter": "0.1.3", + "@next/env": "15.5.2", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -22130,19 +24896,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.2.4", - "@next/swc-darwin-x64": "15.2.4", - "@next/swc-linux-arm64-gnu": "15.2.4", - "@next/swc-linux-arm64-musl": "15.2.4", - "@next/swc-linux-x64-gnu": "15.2.4", - "@next/swc-linux-x64-musl": "15.2.4", - "@next/swc-win32-arm64-msvc": "15.2.4", - "@next/swc-win32-x64-msvc": "15.2.4", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.5.2", + "@next/swc-darwin-x64": "15.5.2", + "@next/swc-linux-arm64-gnu": "15.5.2", + "@next/swc-linux-arm64-musl": "15.5.2", + "@next/swc-linux-x64-gnu": "15.5.2", + "@next/swc-linux-x64-musl": "15.5.2", + "@next/swc-win32-arm64-msvc": "15.5.2", + "@next/swc-win32-x64-msvc": "15.5.2", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -22403,11 +25169,23 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/node-html-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", + "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "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 + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" }, "node_modules/nodemailer": { "version": "6.9.9", @@ -22491,6 +25269,16 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -22529,11 +25317,63 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.18", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.18.tgz", "integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==" }, + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/nypm/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/oauth": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", @@ -25580,6 +28420,15 @@ "node": ">=12" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -25609,6 +28458,50 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, "node_modules/postcss-load-config": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", @@ -25644,6 +28537,53 @@ } } }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -25772,6 +28712,19 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -25798,6 +28751,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -26047,6 +29014,20 @@ "node": ">=6" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -26328,47 +29309,36 @@ } }, "node_modules/react-email": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.7.tgz", - "integrity": "sha512-XCXlfZLKv9gHd/ZwUEhCpRGc/FJLZGYczeuG1kVR/be2PlkwEB4gjX9ARBbRFv86ncbtpOu/wI6jD6kadRyAKw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.0.tgz", + "integrity": "sha512-XFHCSfhdlO7k5q2TYGwC0HsVh5Yn13YaOdahuJEUEOfOJKHEpSP4PKg7R/RiKFoK9cDvzunhY+58pXxz0vE2zA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "7.24.5", - "@babel/traverse": "7.25.6", - "chalk": "4.1.2", - "chokidar": "4.0.3", - "commander": "11.1.0", - "debounce": "2.0.0", - "esbuild": "0.25.0", - "glob": "10.3.4", - "log-symbols": "4.1.0", - "mime-types": "2.1.35", - "next": "15.2.4", - "normalize-path": "3.0.0", - "ora": "5.4.1", - "socket.io": "4.8.1" + "@babel/parser": "^7.27.0", + "@babel/traverse": "^7.27.0", + "chokidar": "^4.0.3", + "commander": "^13.0.0", + "debounce": "^2.0.0", + "esbuild": "^0.25.0", + "glob": "^11.0.0", + "jiti": "2.4.2", + "log-symbols": "^7.0.0", + "mime-types": "^3.0.0", + "normalize-path": "^3.0.0", + "nypm": "0.6.0", + "ora": "^8.0.0", + "prompts": "2.4.2", + "socket.io": "^4.8.1", + "tsconfig-paths": "4.2.0" }, "bin": { - "email": "dist/cli/index.js" + "email": "dist/index.js" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/react-email/node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -26760,80 +29730,6 @@ "node": ">=18" } }, - "node_modules/react-email/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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-email/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, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/react-email/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, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/react-email/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" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/react-email/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, - "license": "MIT", - "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/react-email/node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -26851,26 +29747,29 @@ } }, "node_modules/react-email/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-email/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/react-email/node_modules/esbuild": { @@ -26915,131 +29814,219 @@ } }, "node_modules/react-email/node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-email/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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-email/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/react-email/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/react-email/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-email/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "node_modules/react-email/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/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==", + "node_modules/react-email/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/react-email/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react-email/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 6" + "node": ">= 0.6" + } + }, + "node_modules/react-email/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/react-email/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/react-email/node_modules/readdirp": { @@ -27057,43 +30044,64 @@ } }, "node_modules/react-email/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/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==", + "node_modules/react-email/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/supports-color": { + "node_modules/react-email/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-is": { @@ -27117,6 +30125,98 @@ "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -27698,6 +30798,26 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/scim-patch": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz", @@ -27744,9 +30864,9 @@ "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -27834,6 +30954,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -27896,29 +31026,36 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -27927,40 +31064,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/sharp/node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" } }, "node_modules/shebang-command": { @@ -28104,6 +31229,13 @@ "node": ">=10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/sjcl": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", @@ -28405,6 +31537,40 @@ } } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -28517,6 +31683,17 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sonner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", + "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -28535,6 +31712,23 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spamc": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/spamc/-/spamc-0.0.5.tgz", + "integrity": "sha512-jYXItuZuiWZyG9fIdvgTUbp2MNRuyhuSwvvhhpPJd4JK/9oSZxkD7zAj53GJtowSlXwCJzLg6sCKAoE9wXsKgg==", + "dev": true + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -28637,6 +31831,29 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -28738,15 +31955,6 @@ "node": ">=4.0.0" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -29060,13 +32268,130 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/tailwindcss/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -29253,6 +32578,78 @@ "node": ">= 6" } }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -29316,6 +32713,13 @@ "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", "dev": true }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinypool": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", @@ -29355,6 +32759,20 @@ "resolved": "https://registry.npmjs.org/tlhunter-sorted-set/-/tlhunter-sorted-set-0.1.0.tgz", "integrity": "sha512-eGYW4bjf1DtrHzUYxYfAcSytpOkA44zsr7G2n3PV7yOUR23vmkGe3LL4R+1jL9OsXtbsFOwe8XtbCrabeaEFnw==" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -29571,9 +32989,9 @@ } }, "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsup": { @@ -30262,14 +33680,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -30339,6 +33757,15 @@ "node": ">=14.17" } }, + "node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/ufo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", @@ -30396,12 +33823,12 @@ "dev": true }, "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, "node_modules/undici-types": { @@ -30502,9 +33929,9 @@ } }, "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==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -30520,9 +33947,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -30572,6 +34000,64 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-debounce": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", + "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -31370,6 +34856,20 @@ "node": ">=18" } }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -31385,6 +34885,96 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -31450,15 +35040,18 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -31595,6 +35188,15 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, + "node_modules/wordwrapjs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -31813,6 +35415,15 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xpath": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", @@ -31939,6 +35550,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.24.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", diff --git a/backend/package.json b/backend/package.json index f06c1d69f4..17be524f2b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -79,12 +79,17 @@ "keywords": [], "author": "", "license": "ISC", + "overrides": { + "cipher-base": "1.0.5", + "sha.js": "2.4.12" + }, "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", + "@react-email/preview-server": "^4.3.0", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -120,7 +125,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "4.0.7", + "react-email": "^4.3.0", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -138,7 +143,7 @@ "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", - "@elastic/elasticsearch": "^8.15.0", + "@elastic/elasticsearch": "^9.1.1", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.5.0", "@fastify/etag": "^5.1.0", @@ -185,7 +190,7 @@ "ajv": "^8.12.0", "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "axios-ntlm": "^1.4.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", @@ -196,7 +201,7 @@ "cron": "^3.1.7", "dd-trace": "^5.40.0", "dotenv": "^16.4.1", - "fastify": "^4.28.1", + "fastify": "^4.29.1", "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 111f425203..358a61b31b 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -85,10 +85,9 @@ const getZodDefaultValue = (type: unknown, value: string | number | boolean | Ob }; const bigIntegerColumns: Record = { - "folder_commits": ["commitId"] + folder_commits: ["commitId"] }; - const main = async () => { const tables = ( await db("information_schema.tables") @@ -99,6 +98,7 @@ const main = async () => { (el) => !el.tableName.includes("_migrations") && !el.tableName.includes("audit_logs_") && + !el.tableName.includes("user_notifications_") && !el.tableName.includes("active_locks") && el.tableName !== "intermediate_audit_logs" ); diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 088d99ee97..e3e8733f0f 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -20,8 +20,6 @@ import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2 import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template"; -import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; -import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; @@ -35,7 +33,6 @@ import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-sess import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPitServiceFactory } from "@app/ee/services/pit/pit-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types"; -import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { RateLimitConfiguration, TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-types"; import { TRelayServiceFactory } from "@app/ee/services/relay/relay-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types"; @@ -53,6 +50,7 @@ import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-servi import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; +import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; @@ -65,6 +63,7 @@ import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-a import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; +import { TConvertorServiceFactory } from "@app/services/convertor/convertor-service"; import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service"; @@ -88,10 +87,12 @@ import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-a import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; +import { TMembershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service"; +import { TMembershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service"; +import { TMembershipUserServiceFactory } from "@app/services/membership-user/membership-user-service"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; import { TOfflineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service"; -import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; @@ -104,8 +105,8 @@ import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; -import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { TReminderServiceFactory } from "@app/services/reminder/reminder-types"; +import { TRoleServiceFactory } from "@app/services/role/role-service"; import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; @@ -213,7 +214,6 @@ declare module "fastify" { authToken: TAuthTokenServiceFactory; permission: TPermissionServiceFactory; org: TOrgServiceFactory; - orgRole: TOrgRoleServiceFactory; oidc: TOidcConfigServiceFactory; superAdmin: TSuperAdminServiceFactory; user: TUserServiceFactory; @@ -225,7 +225,6 @@ declare module "fastify" { projectMembership: TProjectMembershipServiceFactory; projectEnv: TProjectEnvServiceFactory; projectKey: TProjectKeyServiceFactory; - projectRole: TProjectRoleServiceFactory; secret: TSecretServiceFactory; secretReplication: TSecretReplicationServiceFactory; secretTag: TSecretTagServiceFactory; @@ -281,9 +280,6 @@ declare module "fastify" { telemetry: TTelemetryServiceFactory; dynamicSecret: TDynamicSecretServiceFactory; dynamicSecretLease: TDynamicSecretLeaseServiceFactory; - projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory; - identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; - identityProjectAdditionalPrivilegeV2: TIdentityProjectAdditionalPrivilegeV2ServiceFactory; secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; @@ -324,6 +320,13 @@ declare module "fastify" { pamAccount: TPamAccountServiceFactory; pamSession: TPamSessionServiceFactory; upgradePath: TUpgradePathService; + + membershipUser: TMembershipUserServiceFactory; + membershipIdentity: TMembershipIdentityServiceFactory; + membershipGroup: TMembershipGroupServiceFactory; + additionalPrivilege: TAdditionalPrivilegeServiceFactory; + role: TRoleServiceFactory; + convertor: TConvertorServiceFactory; }; // 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 c4d45ca276..07fe2c97dd 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -17,6 +17,9 @@ import { TAccessApprovalRequestsReviewersInsert, TAccessApprovalRequestsReviewersUpdate, TAccessApprovalRequestsUpdate, + TAdditionalPrivileges, + TAdditionalPrivilegesInsert, + TAdditionalPrivilegesUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -227,6 +230,15 @@ import { TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate, + TMembershipRoles, + TMembershipRolesInsert, + TMembershipRolesUpdate, + TMemberships, + TMembershipsInsert, + TMembershipsUpdate, + TNamespaces, + TNamespacesInsert, + TNamespacesUpdate, TOidcConfigs, TOidcConfigsInsert, TOidcConfigsUpdate, @@ -314,6 +326,9 @@ import { TResourceMetadata, TResourceMetadataInsert, TResourceMetadataUpdate, + TRoles, + TRolesInsert, + TRolesUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, @@ -1316,5 +1331,19 @@ declare module "knex/types/tables" { [TableName.PamResource]: KnexOriginal.CompositeTableType; [TableName.PamAccount]: KnexOriginal.CompositeTableType; [TableName.PamSession]: KnexOriginal.CompositeTableType; + + [TableName.Namespace]: KnexOriginal.CompositeTableType; + [TableName.Membership]: KnexOriginal.CompositeTableType; + [TableName.MembershipRole]: KnexOriginal.CompositeTableType< + TMembershipRoles, + TMembershipRolesInsert, + TMembershipRolesUpdate + >; + [TableName.Role]: KnexOriginal.CompositeTableType; + [TableName.AdditionalPrivilege]: KnexOriginal.CompositeTableType< + TAdditionalPrivileges, + TAdditionalPrivilegesInsert, + TAdditionalPrivilegesUpdate + >; } } diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index b1ecf7d659..112def0b3d 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -1,5 +1,27 @@ import knex, { Knex } from "knex"; +const parseSslConfig = (dbConnectionUri: string, dbRootCert?: string) => { + let modifiedDbConnectionUri = dbConnectionUri; + let sslConfig: { rejectUnauthorized: boolean; ca: string } | boolean = false; + + if (dbRootCert) { + const url = new URL(dbConnectionUri); + const sslMode = url.searchParams.get("sslmode"); + + if (sslMode && sslMode !== "disable") { + url.searchParams.delete("sslmode"); + modifiedDbConnectionUri = url.toString(); + + sslConfig = { + rejectUnauthorized: ["verify-ca", "verify-full"].includes(sslMode), + ca: Buffer.from(dbRootCert, "base64").toString("ascii") + }; + } + } + + return { modifiedDbConnectionUri, sslConfig }; +}; + export type TDbClient = Knex; export const initDbConnection = ({ dbConnectionUri, @@ -32,23 +54,18 @@ export const initDbConnection = ({ return selectedReplica; }); + const { modifiedDbConnectionUri, sslConfig } = parseSslConfig(dbConnectionUri, dbRootCert); + db = knex({ client: "pg", connection: { - connectionString: dbConnectionUri, + connectionString: modifiedDbConnectionUri, host: process.env.DB_HOST, - // @ts-expect-error I have no clue why only for the port there is a type error - // eslint-disable-next-line - port: process.env.DB_PORT, + port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : undefined, user: process.env.DB_USER, database: process.env.DB_NAME, password: process.env.DB_PASSWORD, - ssl: dbRootCert - ? { - rejectUnauthorized: true, - ca: Buffer.from(dbRootCert, "base64").toString("ascii") - } - : false + ssl: sslConfig }, // https://knexjs.org/guide/#pool pool: { min: 0, max: 10 }, @@ -59,16 +76,16 @@ export const initDbConnection = ({ readReplicaDbs = readReplicas.map((el) => { const replicaDbCertificate = el.dbRootCert || dbRootCert; + const { modifiedDbConnectionUri: replicaUri, sslConfig: replicaSslConfig } = parseSslConfig( + el.dbConnectionUri, + replicaDbCertificate + ); + return knex({ client: "pg", connection: { - connectionString: el.dbConnectionUri, - ssl: replicaDbCertificate - ? { - rejectUnauthorized: true, - ca: Buffer.from(replicaDbCertificate, "base64").toString("ascii") - } - : false + connectionString: replicaUri, + ssl: replicaSslConfig }, migrations: { tableName: "infisical_migrations" @@ -87,26 +104,21 @@ export const initAuditLogDbConnection = ({ dbConnectionUri: string; dbRootCert?: string; }) => { + const { modifiedDbConnectionUri, sslConfig } = parseSslConfig(dbConnectionUri, dbRootCert); + // 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 const db: Knex = knex({ client: "pg", connection: { - connectionString: dbConnectionUri, + connectionString: modifiedDbConnectionUri, host: process.env.AUDIT_LOGS_DB_HOST, - // @ts-expect-error I have no clue why only for the port there is a type error - // eslint-disable-next-line - port: process.env.AUDIT_LOGS_DB_PORT, + port: process.env.AUDIT_LOGS_DB_PORT ? parseInt(process.env.AUDIT_LOGS_DB_PORT, 10) : undefined, user: process.env.AUDIT_LOGS_DB_USER, database: process.env.AUDIT_LOGS_DB_NAME, password: process.env.AUDIT_LOGS_DB_PASSWORD, - ssl: dbRootCert - ? { - rejectUnauthorized: true, - ca: Buffer.from(dbRootCert, "base64").toString("ascii") - } - : false + ssl: sslConfig }, migrations: { tableName: "infisical_migrations" diff --git a/backend/src/db/migrations/20240702131735_secret-approval-groups.ts b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts index 84824ac652..c1b6bb48ba 100644 --- a/backend/src/db/migrations/20240702131735_secret-approval-groups.ts +++ b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts @@ -127,7 +127,8 @@ export async function down(knex: Knex): Promise { }); await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { tb.dropColumn("approverUserId"); - tb.uuid("approverId").notNullable().alter(); + // akhilmhdh: i had to comment this out and is not possible as membership is now changed in structure + // tb.uuid("approverId").notNullable().alter(); }); } } diff --git a/backend/src/db/migrations/20251005152640_simplify-membership.ts b/backend/src/db/migrations/20251005152640_simplify-membership.ts new file mode 100644 index 0000000000..ff2987da71 --- /dev/null +++ b/backend/src/db/migrations/20251005152640_simplify-membership.ts @@ -0,0 +1,1121 @@ +import { Knex } from "knex"; + +import { AccessScope, TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +const createNamespaceTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.Namespace, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.string("description"); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.Namespace); +}; + +const createMembershipTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.Membership, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("scope", 24).notNullable(); + + t.uuid("actorUserId"); + t.foreign("actorUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.uuid("actorIdentityId"); + t.foreign("actorIdentityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.uuid("actorGroupId"); + t.foreign("actorGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); + + t.uuid("scopeOrgId").notNullable(); + t.foreign("scopeOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("scopeProjectId", 36); + t.foreign("scopeProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("scopeNamespaceId"); + t.foreign("scopeNamespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.boolean("isActive").defaultTo(true).notNullable(); + t.string("status"); + t.string("inviteEmail"); + t.datetime("lastInvitedAt"); + t.string("lastLoginAuthMethod"); + t.datetime("lastLoginTime"); + t.specificType("projectFavorites", "text[]"); + t.timestamps(true, true, true); + + t.index(["scope", "scopeOrgId"]); + + t.check( + `(:actorUserIdColumn: IS NOT NULL AND :actorIdentityIdColumn: IS NULL AND :actorGroupIdColumn: IS NULL) OR + (:actorIdentityIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL AND :actorGroupIdColumn: IS NULL) OR + (:actorGroupIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL AND :actorIdentityIdColumn: IS NULL)`, + { + actorUserIdColumn: "actorUserId", + actorIdentityIdColumn: "actorIdentityId", + actorGroupIdColumn: "actorGroupId" + }, + "only_one_actor_type" + ); + + t.check( + `(:scopeColumn: = 'namespace' AND :scopeNamespaceIdColumn: IS NOT NULL) OR + (:scopeColumn: = 'project' AND :scopeProjectIdColumn: IS NOT NULL) OR + (:scopeColumn: = 'organization') + `, + { + scopeColumn: "scope", + scopeNamespaceIdColumn: "scopeNamespaceId", + scopeProjectIdColumn: "scopeProjectId" + }, + "scope_matches_id" + ); + }); + + await createOnUpdateTrigger(knex, TableName.Membership); +}; + +const createRoleTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.Role, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.string("description"); + t.string("slug").notNullable(); + t.jsonb("permissions").notNullable(); + + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("projectId", 36); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("namespaceId"); + t.foreign("namespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.check( + `(:orgIdColumn: IS NOT NULL AND :namespaceIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:namespaceIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:projectIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :namespaceIdColumn: IS NULL)`, + { + orgIdColumn: "orgId", + namespaceIdColumn: "namespaceId", + projectIdColumn: "projectId" + }, + "only_one_scope_id" + ); + + t.timestamps(true, true, true); + }); + + await knex.schema.raw(` + CREATE UNIQUE INDEX role_name_org_id_unique + ON "${TableName.Role}" (slug, "orgId") + WHERE "orgId" IS NOT NULL; + `); + + await knex.schema.raw(` + CREATE UNIQUE INDEX role_name_project_id_unique + ON "${TableName.Role}" (slug, "projectId") + WHERE "projectId" IS NOT NULL; + `); + + await knex.schema.raw(` + CREATE UNIQUE INDEX role_name_namespace_id_unique + ON "${TableName.Role}" (slug, "namespaceId") + WHERE "namespaceId" IS NOT NULL; + `); + + await createOnUpdateTrigger(knex, TableName.Role); +}; + +const createMembershipRoleTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.MembershipRole, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("role").notNullable(); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + + t.uuid("customRoleId"); + t.foreign("customRoleId").references("id").inTable(TableName.Role); + t.uuid("membershipId").notNullable(); + t.foreign("membershipId").references("id").inTable(TableName.Membership).onDelete("CASCADE"); + + t.index("membershipId"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.MembershipRole); +}; + +const createAdditionalPrivilegeTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.AdditionalPrivilege, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name", 60).notNullable(); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.jsonb("permissions").notNullable(); + + t.uuid("actorUserId"); + t.foreign("actorUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.uuid("actorIdentityId"); + t.foreign("actorIdentityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("projectId", 36); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("namespaceId"); + t.foreign("namespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.check( + `(:orgIdColumn: IS NOT NULL AND :namespaceIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:namespaceIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:projectIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :namespaceIdColumn: IS NULL)`, + { + orgIdColumn: "orgId", + namespaceIdColumn: "namespaceId", + projectIdColumn: "projectId" + }, + "only_one_scope_id" + ); + + t.check( + `(:actorUserIdColumn: IS NOT NULL AND :actorIdentityIdColumn: IS NULL) OR + (:actorIdentityIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL) + `, + { + actorUserIdColumn: "actorUserId", + actorIdentityIdColumn: "actorIdentityId" + }, + "only_one_actor_type" + ); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.AdditionalPrivilege); +}; + +const migrateMembershipData = async (knex: Knex) => { + await knex + .insert( + knex(TableName.OrgMembership).select( + "id", + "status", + "inviteEmail", + "createdAt", + "updatedAt", + "userId", + "orgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + knex.raw("?", [AccessScope.Organization]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "status", + "inviteEmail", + "createdAt", + "updatedAt", + "actorUserId", + "scopeOrgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityOrgMembership).select( + "id", + "identityId", + "orgId", + "lastLoginAuthMethod", + "lastLoginTime", + "createdAt", + "updatedAt", + knex.raw("?", [AccessScope.Organization]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorIdentityId", + "scopeOrgId", + "lastLoginAuthMethod", + "lastLoginTime", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.Groups).select("id", "orgId", "createdAt", "updatedAt", knex.raw("?", [AccessScope.Organization])) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??)", [ + TableName.Membership, + "actorGroupId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.ProjectMembership) + .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .select( + knex.ref("id").withSchema(TableName.ProjectMembership), + "userId", + "projectId", + "orgId", + knex.ref("createdAt").withSchema(TableName.ProjectMembership), + knex.ref("updatedAt").withSchema(TableName.ProjectMembership), + knex.raw("?", [AccessScope.Project]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorUserId", + "scopeProjectId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityProjectMembership) + .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .select( + knex.ref("id").withSchema(TableName.IdentityProjectMembership), + "identityId", + "projectId", + "orgId", + knex.ref("createdAt").withSchema(TableName.IdentityProjectMembership), + knex.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), + knex.raw("?", [AccessScope.Project]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorIdentityId", + "scopeProjectId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.GroupProjectMembership) + .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) + .select( + knex.ref("id").withSchema(TableName.GroupProjectMembership), + "groupId", + "projectId", + "orgId", + knex.ref("createdAt").withSchema(TableName.GroupProjectMembership), + knex.ref("updatedAt").withSchema(TableName.GroupProjectMembership), + knex.raw("?", [AccessScope.Project]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorGroupId", + "scopeProjectId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); +}; + +const migrateRoleData = async (knex: Knex) => { + await knex + .insert( + knex(TableName.OrgRoles).select( + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "orgId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Role, + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "orgId" + ]) + ); + + await knex + .insert( + knex(TableName.ProjectRoles).select( + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "projectId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Role, + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "projectId" + ]) + ); + + const hasExternalGroupRoleMappingRoleColumn = await knex.schema.hasColumn( + TableName.ExternalGroupOrgRoleMapping, + "roleId" + ); + if (hasExternalGroupRoleMappingRoleColumn) { + await knex.schema.alterTable(TableName.ExternalGroupOrgRoleMapping, (t) => { + t.dropForeign("roleId"); + t.foreign("roleId").references("id").inTable(TableName.Role); + }); + } +}; + +const migrateMembershipRoleData = async (knex: Knex) => { + await knex + .insert(knex(TableName.OrgMembership).select("id", "role", "roleId")) + .into(knex.raw("?? (??, ??, ??)", [TableName.MembershipRole, "membershipId", "role", "customRoleId"])); + + await knex + .insert(knex(TableName.IdentityOrgMembership).select("id", "role", "roleId")) + .into(knex.raw("?? (??, ??, ??)", [TableName.MembershipRole, "membershipId", "role", "customRoleId"])); + + await knex + .insert( + knex(TableName.Groups) + .join(TableName.Membership, (qb) => { + qb.on(`${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .andOn(`${TableName.Groups}.orgId`, `${TableName.Membership}.scopeOrgId`) + .andOn(`${TableName.Membership}.scope`, knex.raw("?", [AccessScope.Organization])); + }) + .select(knex.ref("id").withSchema(TableName.Membership), "role", "roleId") + ) + .into(knex.raw("?? (??, ??, ??)", [TableName.MembershipRole, "membershipId", "role", "customRoleId"])); + + await knex + .insert( + knex(TableName.ProjectUserMembershipRole).select( + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.MembershipRole, + "id", + "role", + "membershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityProjectMembershipRole).select( + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.MembershipRole, + "id", + "role", + "membershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]) + ); + + await knex + .insert( + knex(TableName.GroupProjectMembershipRole).select( + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.MembershipRole, + "id", + "role", + "membershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]) + ); +}; + +const migrateAdditionalPrivilegeData = async (knex: Knex) => { + await knex + .insert( + knex(TableName.ProjectUserAdditionalPrivilege).select( + "id", + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "userId", + "projectId", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.AdditionalPrivilege, + "id", + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "actorUserId", + "projectId", + "createdAt", + "updatedAt" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityProjectAdditionalPrivilege) + .join( + TableName.IdentityProjectMembership, + `${TableName.IdentityProjectMembership}.id`, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` + ) + .select( + knex.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege), + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "identityId", + "projectId", + knex.ref("createdAt").withSchema(TableName.IdentityProjectAdditionalPrivilege), + knex.ref("updatedAt").withSchema(TableName.IdentityProjectAdditionalPrivilege) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.AdditionalPrivilege, + "id", + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "actorIdentityId", + "projectId", + "createdAt", + "updatedAt" + ]) + ); + + const hasApColumnInAccessApprovalRequest = await knex.schema.hasColumn( + TableName.AccessApprovalRequest, + "privilegeId" + ); + if (hasApColumnInAccessApprovalRequest) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.dropForeign("privilegeId"); + t.foreign("privilegeId").references("id").inTable(TableName.AdditionalPrivilege); + }); + } +}; + +export async function up(knex: Knex): Promise { + const hasToMigrateNamespaceTable = !(await knex.schema.hasTable(TableName.Namespace)); + if (hasToMigrateNamespaceTable) { + await createNamespaceTable(knex); + } + + const hasToMigrateMembershipTable = !(await knex.schema.hasTable(TableName.Membership)); + if (hasToMigrateMembershipTable) { + await createMembershipTable(knex); + } + + const hasToMigrateRoleTable = !(await knex.schema.hasTable(TableName.Role)); + if (hasToMigrateRoleTable) { + await createRoleTable(knex); + } + + const hasToMigrateMembershipRoleTable = !(await knex.schema.hasTable(TableName.MembershipRole)); + if (hasToMigrateMembershipRoleTable) { + await createMembershipRoleTable(knex); + } + + const hasToMigrateAdditionalPrivilegeTable = !(await knex.schema.hasTable(TableName.AdditionalPrivilege)); + if (hasToMigrateAdditionalPrivilegeTable) { + await createAdditionalPrivilegeTable(knex); + } + + // this means these tables have been created before + if (hasToMigrateMembershipTable) { + await migrateMembershipData(knex); + } + + if (hasToMigrateRoleTable) { + await migrateRoleData(knex); + } + + if (hasToMigrateMembershipRoleTable) { + await migrateMembershipRoleData(knex); + } + + if (hasToMigrateAdditionalPrivilegeTable) { + await migrateAdditionalPrivilegeData(knex); + } +} + +const rollbackAdditionalPrivilegeData = async (knex: Knex) => { + const projectUserAdditionalPrivilegeFields = [ + "id", + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "userId", + "projectId", + "createdAt", + "updatedAt" + ]; + + await knex + .insert( + knex(TableName.AdditionalPrivilege) + .whereNotNull("actorUserId") + .whereNotNull("projectId") + .select( + "id", + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "actorUserId", + "projectId", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.ProjectUserAdditionalPrivilege, + ...projectUserAdditionalPrivilegeFields + ]) + ) + .onConflict("id") + .merge(projectUserAdditionalPrivilegeFields); + + const identityProjectAdditionalPrivilegeFields = [ + "id", + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "projectMembershipId", + "createdAt", + "updatedAt" + ]; + + await knex + .insert( + knex(TableName.AdditionalPrivilege) + .join(TableName.Membership, (qb) => { + qb.on(`${TableName.AdditionalPrivilege}.actorIdentityId`, `${TableName.Membership}.actorIdentityId`) + .andOn(`${TableName.AdditionalPrivilege}.projectId`, `${TableName.Membership}.scopeProjectId`) + .andOn(`${TableName.Membership}.scope`, knex.raw("?", [AccessScope.Project])); + }) + .whereNotNull(`${TableName.AdditionalPrivilege}.actorIdentityId`) + .whereNotNull(`${TableName.AdditionalPrivilege}.projectId`) + .select( + knex.ref("id").withSchema(TableName.AdditionalPrivilege), + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + knex.ref("id").withSchema(TableName.Membership).as("projectMembershipId"), + knex.ref("createdAt").withSchema(TableName.AdditionalPrivilege), + knex.ref("updatedAt").withSchema(TableName.AdditionalPrivilege) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.IdentityProjectAdditionalPrivilege, + ...identityProjectAdditionalPrivilegeFields + ]) + ) + .onConflict("id") + .merge(identityProjectAdditionalPrivilegeFields); +}; + +const rollbackMembershipRoleData = async (knex: Knex) => { + const groupRoleFields = ["id", "name", "slug", "createdAt", "updatedAt", "role", "roleId", "orgId"]; + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .join(TableName.Groups, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .select( + knex.ref("actorGroupId").withSchema(TableName.Membership), + knex.ref("name").withSchema(TableName.Groups), + knex.ref("slug").withSchema(TableName.Groups), + knex.ref("createdAt").withSchema(TableName.Groups), + knex.ref("updatedAt").withSchema(TableName.Groups), + knex.ref("role").withSchema(TableName.MembershipRole), + "customRoleId", + knex.ref("orgId").withSchema(TableName.Groups) + ) + ) + .into(knex.raw("?? (??,??,??,??,??,??,??,??)", [TableName.Groups, ...groupRoleFields])) + .onConflict("id") + .merge(groupRoleFields); + + const projectMembershipRoleFields = [ + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]; + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .select( + knex.ref("id").withSchema(TableName.MembershipRole), + "role", + knex.ref("membershipId").withSchema(TableName.MembershipRole), + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + knex.ref("createdAt").withSchema(TableName.MembershipRole), + knex.ref("updatedAt").withSchema(TableName.MembershipRole) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.ProjectUserMembershipRole, + ...projectMembershipRoleFields + ]) + ) + .onConflict("id") + .merge(projectMembershipRoleFields); + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .select( + knex.ref("id").withSchema(TableName.MembershipRole), + "role", + knex.ref("membershipId").withSchema(TableName.MembershipRole), + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + knex.ref("createdAt").withSchema(TableName.MembershipRole), + knex.ref("updatedAt").withSchema(TableName.MembershipRole) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.IdentityProjectMembershipRole, + ...projectMembershipRoleFields + ]) + ) + .onConflict("id") + .merge(projectMembershipRoleFields); + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .select( + knex.ref("id").withSchema(TableName.MembershipRole), + "role", + knex.ref("membershipId").withSchema(TableName.MembershipRole), + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + knex.ref("createdAt").withSchema(TableName.MembershipRole), + knex.ref("updatedAt").withSchema(TableName.MembershipRole) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.GroupProjectMembershipRole, + ...projectMembershipRoleFields + ]) + ) + .onConflict("id") + .merge(projectMembershipRoleFields); +}; + +const rollbackRoleData = async (knex: Knex) => { + const orgRoleFields = ["id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "orgId"]; + + await knex + .insert( + knex(TableName.Role) + .whereNotNull("orgId") + .select("id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "orgId") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [TableName.OrgRoles, ...orgRoleFields])) + .onConflict("id") + .merge(orgRoleFields); + + const projectRoleFields = ["id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "projectId"]; + + await knex + .insert( + knex(TableName.Role) + .whereNotNull("projectId") + .select("id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "projectId") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [TableName.ProjectRoles, ...projectRoleFields])) + .onConflict("id") + .merge(projectRoleFields); +}; + +const rollbackMembershipData = async (knex: Knex) => { + const orgMembershipFields = [ + "id", + "status", + "inviteEmail", + "createdAt", + "updatedAt", + "userId", + "orgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + "role", + "roleId" + ]; + await knex + .insert( + knex(TableName.Membership) + .leftJoin(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .select( + knex.ref("id").withSchema(TableName.Membership), + "status", + "inviteEmail", + knex.ref("createdAt").withSchema(TableName.Membership), + knex.ref("updatedAt").withSchema(TableName.Membership), + "actorUserId", + "scopeOrgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + "role", + "customRoleId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.OrgMembership, + ...orgMembershipFields + ]) + ) + .onConflict("id") + .merge(orgMembershipFields); + + const identityOrgMembershipFields = [ + "id", + "identityId", + "orgId", + "lastLoginAuthMethod", + "lastLoginTime", + "createdAt", + "updatedAt", + "role", + "roleId" + ]; + + await knex + .insert( + knex(TableName.Membership) + .leftJoin(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .select( + knex.ref("id").withSchema(TableName.Membership), + "actorIdentityId", + "scopeOrgId", + "lastLoginAuthMethod", + "lastLoginTime", + knex.ref("createdAt").withSchema(TableName.Membership), + knex.ref("updatedAt").withSchema(TableName.Membership), + "role", + "customRoleId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.IdentityOrgMembership, + ...identityOrgMembershipFields + ]) + ) + .onConflict("id") + .merge(identityOrgMembershipFields); + + const projectMembershipFields = ["id", "userId", "projectId", "createdAt", "updatedAt"]; + + await knex + .insert( + knex(TableName.Membership) + .where("scope", AccessScope.Project) + .whereNotNull("actorUserId") + .select("id", "actorUserId", "scopeProjectId", "createdAt", "updatedAt") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??)", [TableName.ProjectMembership, ...projectMembershipFields])) + .onConflict("id") + .merge(projectMembershipFields); + + const identityProjectMembershipFields = ["id", "identityId", "projectId", "createdAt", "updatedAt"]; + + await knex + .insert( + knex(TableName.Membership) + .where("scope", AccessScope.Project) + .whereNotNull("actorIdentityId") + .select("id", "actorIdentityId", "scopeProjectId", "createdAt", "updatedAt") + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??)", [TableName.IdentityProjectMembership, ...identityProjectMembershipFields]) + ) + .onConflict("id") + .merge(identityProjectMembershipFields); + + const groupProjectMembershipFields = ["id", "groupId", "projectId", "createdAt", "updatedAt"]; + + await knex + .insert( + knex(TableName.Membership) + .where("scope", AccessScope.Project) + .whereNotNull("actorGroupId") + .select("id", "actorGroupId", "scopeProjectId", "createdAt", "updatedAt") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??)", [TableName.GroupProjectMembership, ...groupProjectMembershipFields])) + .onConflict("id") + .merge(groupProjectMembershipFields); +}; + +export async function down(knex: Knex): Promise { + const hasRoleTable = await knex.schema.hasTable(TableName.Role); + if (hasRoleTable) { + await rollbackRoleData(knex); + } + + const hasMembershipTable = await knex.schema.hasTable(TableName.Membership); + if (hasMembershipTable) { + await rollbackMembershipData(knex); + } + + const hasMembershipRoleTable = await knex.schema.hasTable(TableName.MembershipRole); + if (hasMembershipRoleTable) { + await rollbackMembershipRoleData(knex); + } + + const hasAdditionalPrivilegeTable = await knex.schema.hasTable(TableName.AdditionalPrivilege); + if (hasAdditionalPrivilegeTable) { + await rollbackAdditionalPrivilegeData(knex); + } + + // Restore foreign key references + const hasApColumnInAccessApprovalRequest = await knex.schema.hasColumn( + TableName.AccessApprovalRequest, + "privilegeId" + ); + if (hasApColumnInAccessApprovalRequest) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.dropForeign("privilegeId"); + t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege); + }); + } + + const hasExternalGroupRoleMappingRoleColumn = await knex.schema.hasColumn( + TableName.ExternalGroupOrgRoleMapping, + "roleId" + ); + if (hasExternalGroupRoleMappingRoleColumn) { + await knex.schema.alterTable(TableName.ExternalGroupOrgRoleMapping, (t) => { + t.dropForeign("roleId"); + t.foreign("roleId").references("id").inTable(TableName.OrgRoles); + }); + } + + // Drop new tables + await dropOnUpdateTrigger(knex, TableName.AdditionalPrivilege); + await knex.schema.dropTableIfExists(TableName.AdditionalPrivilege); + + await dropOnUpdateTrigger(knex, TableName.MembershipRole); + await knex.schema.dropTableIfExists(TableName.MembershipRole); + + await dropOnUpdateTrigger(knex, TableName.Membership); + await knex.schema.dropTableIfExists(TableName.Membership); + + await dropOnUpdateTrigger(knex, TableName.Role); + await knex.schema.dropTableIfExists(TableName.Role); + + await dropOnUpdateTrigger(knex, TableName.Namespace); + await knex.schema.dropTableIfExists(TableName.Namespace); +} diff --git a/backend/src/db/migrations/20251008003912_relay-heartbeat.ts b/backend/src/db/migrations/20251008003912_relay-heartbeat.ts new file mode 100644 index 0000000000..f19c540cbd --- /dev/null +++ b/backend/src/db/migrations/20251008003912_relay-heartbeat.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.Relay, "heartbeat"))) { + await knex.schema.alterTable(TableName.Relay, (t) => { + t.datetime("heartbeat"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Relay, "heartbeat")) { + await knex.schema.alterTable(TableName.Relay, (t) => { + t.dropColumn("heartbeat"); + }); + } +} diff --git a/backend/src/db/migrations/20251008220303_relay-gateway-health-alarm.ts b/backend/src/db/migrations/20251008220303_relay-gateway-health-alarm.ts new file mode 100644 index 0000000000..2abc1cf693 --- /dev/null +++ b/backend/src/db/migrations/20251008220303_relay-gateway-health-alarm.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Relay, "healthAlertedAt"))) { + await knex.schema.alterTable(TableName.Relay, (t) => { + t.datetime("healthAlertedAt"); + }); + } + if (!(await knex.schema.hasColumn(TableName.GatewayV2, "healthAlertedAt"))) { + await knex.schema.alterTable(TableName.GatewayV2, (t) => { + t.datetime("healthAlertedAt"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.GatewayV2, "healthAlertedAt")) { + await knex.schema.alterTable(TableName.GatewayV2, (t) => { + t.dropColumn("healthAlertedAt"); + }); + } + if (await knex.schema.hasColumn(TableName.Relay, "healthAlertedAt")) { + await knex.schema.alterTable(TableName.Relay, (t) => { + t.dropColumn("healthAlertedAt"); + }); + } +} diff --git a/backend/src/db/migrations/20251010111410_add-kmip-metadata.ts b/backend/src/db/migrations/20251010111410_add-kmip-metadata.ts new file mode 100644 index 0000000000..2875cc0adc --- /dev/null +++ b/backend/src/db/migrations/20251010111410_add-kmip-metadata.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.KmsKey, "kmipMetadata"))) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.jsonb("kmipMetadata"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.KmsKey, "kmipMetadata")) { + await knex.schema.alterTable(TableName.KmsKey, (t) => { + t.dropColumn("kmipMetadata"); + }); + } +} diff --git a/backend/src/db/schemas/additional-privileges.ts b/backend/src/db/schemas/additional-privileges.ts new file mode 100644 index 0000000000..8013a6961f --- /dev/null +++ b/backend/src/db/schemas/additional-privileges.ts @@ -0,0 +1,30 @@ +// 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 AdditionalPrivilegesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + permissions: z.unknown(), + actorUserId: z.string().uuid().nullable().optional(), + actorIdentityId: z.string().uuid().nullable().optional(), + orgId: z.string().uuid().nullable().optional(), + projectId: z.string().nullable().optional(), + namespaceId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAdditionalPrivileges = z.infer; +export type TAdditionalPrivilegesInsert = Omit, TImmutableDBKeys>; +export type TAdditionalPrivilegesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/gateways-v2.ts b/backend/src/db/schemas/gateways-v2.ts index 1362793f60..4a48e3ce52 100644 --- a/backend/src/db/schemas/gateways-v2.ts +++ b/backend/src/db/schemas/gateways-v2.ts @@ -18,7 +18,8 @@ export const GatewaysV2Schema = z.object({ relayId: z.string().uuid().nullable().optional(), name: z.string(), heartbeat: z.date().nullable().optional(), - encryptedPamSessionKey: zodBuffer.nullable().optional() + encryptedPamSessionKey: zodBuffer.nullable().optional(), + healthAlertedAt: z.date().nullable().optional() }); export type TGatewaysV2 = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index f8ac885b42..2a10e0f1b5 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -3,6 +3,7 @@ export * from "./access-approval-policies-approvers"; export * from "./access-approval-policies-bypassers"; export * from "./access-approval-requests"; export * from "./access-approval-requests-reviewers"; +export * from "./additional-privileges"; export * from "./api-keys"; export * from "./app-connections"; export * from "./audit-log-streams"; @@ -73,8 +74,11 @@ export * from "./kms-keys"; export * from "./kms-root-config"; export * from "./ldap-configs"; export * from "./ldap-group-maps"; +export * from "./membership-roles"; +export * from "./memberships"; export * from "./microsoft-teams-integrations"; export * from "./models"; +export * from "./namespaces"; export * from "./oidc-configs"; export * from "./org-bots"; export * from "./org-gateway-config"; @@ -108,6 +112,7 @@ export * from "./projects"; export * from "./rate-limit"; export * from "./relays"; export * from "./resource-metadata"; +export * from "./roles"; export * from "./saml-configs"; export * from "./scim-tokens"; export * from "./secret-approval-policies"; diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index ccb779d572..45ff699971 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -17,7 +17,8 @@ export const KmsKeysSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), projectId: z.string().nullable().optional(), - keyUsage: z.string().default("encrypt-decrypt") + keyUsage: z.string().default("encrypt-decrypt"), + kmipMetadata: z.unknown().nullable().optional() }); export type TKmsKeys = z.infer; diff --git a/backend/src/db/schemas/membership-roles.ts b/backend/src/db/schemas/membership-roles.ts new file mode 100644 index 0000000000..98927c9cbd --- /dev/null +++ b/backend/src/db/schemas/membership-roles.ts @@ -0,0 +1,26 @@ +// 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 MembershipRolesSchema = z.object({ + id: z.string().uuid(), + role: z.string(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + customRoleId: z.string().uuid().nullable().optional(), + membershipId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TMembershipRoles = z.infer; +export type TMembershipRolesInsert = Omit, TImmutableDBKeys>; +export type TMembershipRolesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/memberships.ts b/backend/src/db/schemas/memberships.ts new file mode 100644 index 0000000000..e871a5627e --- /dev/null +++ b/backend/src/db/schemas/memberships.ts @@ -0,0 +1,32 @@ +// 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 MembershipsSchema = z.object({ + id: z.string().uuid(), + scope: z.string(), + actorUserId: z.string().uuid().nullable().optional(), + actorIdentityId: z.string().uuid().nullable().optional(), + actorGroupId: z.string().uuid().nullable().optional(), + scopeOrgId: z.string().uuid(), + scopeProjectId: z.string().nullable().optional(), + scopeNamespaceId: z.string().uuid().nullable().optional(), + isActive: z.boolean().default(true), + status: z.string().nullable().optional(), + inviteEmail: z.string().nullable().optional(), + lastInvitedAt: z.date().nullable().optional(), + lastLoginAuthMethod: z.string().nullable().optional(), + lastLoginTime: z.date().nullable().optional(), + projectFavorites: z.string().array().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TMemberships = z.infer; +export type TMembershipsInsert = Omit, TImmutableDBKeys>; +export type TMembershipsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 09ecb367a1..d3537b0282 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -178,6 +178,14 @@ export enum TableName { SecretScanningScan = "secret_scanning_scans", SecretScanningFinding = "secret_scanning_findings", SecretScanningConfig = "secret_scanning_configs", + + Membership = "memberships", + MembershipRole = "membership_roles", + Role = "roles", + AdditionalPrivilege = "additional_privileges", + + Namespace = "namespaces", + // reminders Reminder = "reminders", ReminderRecipient = "reminders_recipients", @@ -302,7 +310,39 @@ export enum ActionProjectType { Any = "any" } +export enum TemporaryPermissionMode { + Relative = "relative" +} + +export enum MembershipActors { + Group = "group", + User = "user", + Identity = "identity" +} + export enum SortDirection { ASC = "asc", DESC = "desc" } + +export enum AccessScope { + Organization = "organization", + Namespace = "namespace", + Project = "project" +} + +export type AccessScopeData = + | { + scope: AccessScope.Organization; + orgId: string; + } + | { + scope: AccessScope.Namespace; + orgId: string; + namespaceId: string; + } + | { + scope: AccessScope.Project; + orgId: string; + projectId: string; + }; diff --git a/backend/src/db/schemas/namespaces.ts b/backend/src/db/schemas/namespaces.ts new file mode 100644 index 0000000000..b9f092fd3e --- /dev/null +++ b/backend/src/db/schemas/namespaces.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const NamespacesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TNamespaces = z.infer; +export type TNamespacesInsert = Omit, TImmutableDBKeys>; +export type TNamespacesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/relays.ts b/backend/src/db/schemas/relays.ts index 4bb615e969..82a5fa8303 100644 --- a/backend/src/db/schemas/relays.ts +++ b/backend/src/db/schemas/relays.ts @@ -14,7 +14,9 @@ export const RelaysSchema = z.object({ orgId: z.string().uuid().nullable().optional(), identityId: z.string().uuid().nullable().optional(), name: z.string(), - host: z.string() + host: z.string(), + heartbeat: z.date().nullable().optional(), + healthAlertedAt: z.date().nullable().optional() }); export type TRelays = z.infer; diff --git a/backend/src/db/schemas/roles.ts b/backend/src/db/schemas/roles.ts new file mode 100644 index 0000000000..4801a57a66 --- /dev/null +++ b/backend/src/db/schemas/roles.ts @@ -0,0 +1,25 @@ +// 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 RolesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + slug: z.string(), + permissions: z.unknown(), + orgId: z.string().uuid().nullable().optional(), + projectId: z.string().nullable().optional(), + namespaceId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TRoles = z.infer; +export type TRolesInsert = Omit, TImmutableDBKeys>; +export type TRolesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/user-notifications-default.ts b/backend/src/db/schemas/user-notifications-default.ts new file mode 100644 index 0000000000..eabcdabc55 --- /dev/null +++ b/backend/src/db/schemas/user-notifications-default.ts @@ -0,0 +1,27 @@ +// 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 UserNotificationsDefaultSchema = z.object({ + id: z.string().uuid(), + userId: z.string().uuid(), + orgId: z.string().uuid().nullable().optional(), + type: z.string(), + title: z.string(), + body: z.string().nullable().optional(), + link: z.string().nullable().optional(), + isRead: z.boolean().default(false), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TUserNotificationsDefault = z.infer; +export type TUserNotificationsDefaultInsert = Omit, TImmutableDBKeys>; +export type TUserNotificationsDefaultUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/seeds/2-org.ts b/backend/src/db/seeds/2-org.ts index a02224dbca..99bd70f576 100644 --- a/backend/src/db/seeds/2-org.ts +++ b/backend/src/db/seeds/2-org.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas"; +import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export async function seed(knex: Knex): Promise { @@ -24,13 +24,22 @@ export async function seed(knex: Knex): Promise { ]) .returning("*"); - await knex(TableName.OrgMembership).insert([ + const [membership] = await knex(TableName.Membership) + .insert([ + { + scope: AccessScope.Organization, + scopeOrgId: org.id, + actorUserId: user.id, + isActive: true, + status: OrgMembershipStatus.Accepted + } + ]) + .returning("*"); + + await knex(TableName.MembershipRole).insert([ { - role: OrgMembershipRole.Admin, - orgId: org.id, - status: OrgMembershipStatus.Accepted, - userId: user.id, - isActive: true + membershipId: membership.id, + role: OrgMembershipRole.Admin } ]); } diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 47a41a95cb..d0294022f2 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -6,14 +6,15 @@ import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { initLogger, logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AuthMethod } from "@app/services/auth/auth-type"; +import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns"; import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"; -import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; -import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { userDALFactory } from "@app/services/user/user-dal"; import { + AccessScope, OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, @@ -39,8 +40,8 @@ const createUserWithGhostUser = async ( ) => { const projectKeyDAL = projectKeyDALFactory(knex); const userDAL = userDALFactory(knex); - const projectMembershipDAL = projectMembershipDALFactory(knex); - const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex); + const membershipDAL = membershipUserDALFactory(knex); + const membershipRoleDAL = membershipRoleDALFactory(knex); const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. @@ -63,25 +64,36 @@ const createUserWithGhostUser = async ( .onConflict("userId") .merge(); - await knex(TableName.OrgMembership) + const [orgMembership] = await knex(TableName.Membership) .insert({ - orgId, - userId: ghostUser.id, - role: OrgMembershipRole.Admin, + scope: AccessScope.Organization, + scopeOrgId: orgId, + actorUserId: ghostUser.id, status: OrgMembershipStatus.Accepted, isActive: true }) .returning("*"); - const [projectMembership] = await knex(TableName.ProjectMembership) + await knex(TableName.MembershipRole).insert([ + { + membershipId: orgMembership.id, + role: OrgMembershipRole.Admin + } + ]); + + const [projectMembership] = await knex(TableName.Membership) .insert({ - userId: ghostUser.id, - projectId + actorUserId: ghostUser.id, + scopeProjectId: projectId, + scope: AccessScope.Project, + scopeOrgId: orgId, + status: OrgMembershipStatus.Accepted, + isActive: true }) .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ - projectMembershipId: projectMembership.id, + await knex(TableName.MembershipRole).insert({ + membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }); @@ -142,17 +154,16 @@ const createUserWithGhostUser = async ( }); // Create a membership for the user - const userProjectMembership = await projectMembershipDAL.create( + const userProjectMembership = await membershipDAL.create( { - projectId, - userId: user.id + scopeProjectId: projectId, + scope: AccessScope.Project, + actorUserId: user.id, + scopeOrgId: orgId }, knex ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, - knex - ); + await membershipRoleDAL.create({ membershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, knex); // Create a project key for the user await projectKeyDAL.create( @@ -195,10 +206,11 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const userOrgMembership = await knex(TableName.OrgMembership) + const userOrgMembership = await knex(TableName.Membership) .where({ - orgId: seedData1.organization.id, - userId: seedData1.id + scopeOrgId: seedData1.organization.id, + actorUserId: seedData1.id, + scope: AccessScope.Organization }) .first(); diff --git a/backend/src/db/seeds/4-project-v3.ts b/backend/src/db/seeds/4-project-v3.ts index f89b965a6b..920497a82b 100644 --- a/backend/src/db/seeds/4-project-v3.ts +++ b/backend/src/db/seeds/4-project-v3.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; +import { AccessScope, ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ @@ -23,15 +23,17 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const projectMembershipV3 = await knex(TableName.ProjectMembership) + const projectMembershipV3 = await knex(TableName.Membership) .insert({ - projectId: projectV2.id, - userId: seedData1.id + scopeProjectId: projectV2.id, + actorUserId: seedData1.id, + scope: AccessScope.Project, + scopeOrgId: seedData1.organization.id }) .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: projectMembershipV3[0].id + membershipId: projectMembershipV3[0].id }); // create default environments and default folders diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index ae3d04514a..333fc7e3a6 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -5,13 +5,12 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { initLogger, logger } from "@app/lib/logger"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; -import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; +import { AccessScope, IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export async function seed(knex: Knex): Promise { // Deletes ALL existing entries await knex(TableName.Identity).del(); - await knex(TableName.IdentityOrgMembership).del(); initLogger(); @@ -78,34 +77,47 @@ export async function seed(knex: Knex): Promise { isClientSecretRevoked: false } ]); - await knex(TableName.IdentityOrgMembership).insert([ + const [orgMembership] = await knex(TableName.Membership) + .insert([ + { + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Organization + } + ]) + .returning("*"); + await knex(TableName.MembershipRole).insert([ { - identityId: seedData1.machineIdentity.id, - orgId: seedData1.organization.id, + membershipId: orgMembership.id, role: OrgMembershipRole.Admin } ]); - const identityProjectMembership = await knex(TableName.IdentityProjectMembership) + const identityProjectMembership = await knex(TableName.Membership) .insert({ - identityId: seedData1.machineIdentity.id, - projectId: seedData1.project.id + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Project, + scopeProjectId: seedData1.project.id }) .returning("*"); - await knex(TableName.IdentityProjectMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: identityProjectMembership[0].id + membershipId: identityProjectMembership[0].id }); - const identityProjectMembershipV3 = await knex(TableName.IdentityProjectMembership) + + const identityProjectMembershipV3 = await knex(TableName.Membership) .insert({ - identityId: seedData1.machineIdentity.id, - projectId: seedData1.projectV3.id + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Project, + scopeProjectId: seedData1.projectV3.id }) .returning("*"); - await knex(TableName.IdentityProjectMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: identityProjectMembershipV3[0].id + membershipId: identityProjectMembershipV3[0].id }); } diff --git a/backend/src/ee/routes/v1/deprecated-project-role-router.ts b/backend/src/ee/routes/v1/deprecated-project-role-router.ts index dc361626d8..f8f73579ee 100644 --- a/backend/src/ee/routes/v1/deprecated-project-role-router.ts +++ b/backend/src/ee/routes/v1/deprecated-project-role-router.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { backfillPermissionV1SchemaToV2Schema, @@ -13,7 +13,6 @@ import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ @@ -55,14 +54,16 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)) ); - const role = await server.services.projectRole.createRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId + }); + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId }, data: { ...req.body, @@ -73,7 +74,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId, event: { type: EventType.CREATE_PROJECT_ROLE, metadata: { @@ -86,7 +87,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -131,12 +132,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) : undefined; - const role = await server.services.projectRole.updateRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId, + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId + }); + + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + selector: { + id: req.params.roleId + }, data: { ...req.body, permissions: stringifiedPermissions @@ -146,7 +156,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.UPDATE_PROJECT_ROLE, metadata: { @@ -159,7 +169,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -188,18 +198,27 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.deleteRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId + }); + + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + selector: { + id: req.params.roleId + } }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.DELETE_PROJECT_ROLE, metadata: { @@ -210,7 +229,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -238,17 +257,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectRole.listRoles({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug - } + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId }); - return { roles }; + + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + data: {} + }); + return { roles: roles.map((el) => ({ ...el, projectId: el.projectId as string })) }; } }); @@ -265,78 +288,30 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }), response: { 200: z.object({ - role: SanitizedRoleSchemaV1.omit({ version: true }) + role: SanitizedRoleSchemaV1 }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.getRoleBySlug({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug - }, - roleSlug: req.params.slug + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId }); - return { role }; - } - }); - - server.route({ - method: "GET", - url: "/:projectId/permissions", - config: { - rateLimit: readLimit - }, - schema: { - params: z.object({ - projectId: z.string().trim() - }), - response: { - 200: z.object({ - data: z.object({ - membership: z.object({ - id: z.string(), - roles: z - .object({ - role: z.string() - }) - .array() - }), - assumedPrivilegeDetails: z - .object({ - actorId: z.string(), - actorType: z.string(), - actorName: z.string(), - actorEmail: z.string().optional() - }) - .optional(), - permissions: z.any().array() - }) - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission( - req.permission.id, - req.params.projectId, - req.permission.authMethod, - req.permission.orgId - ); - - return { - data: { - permissions, - membership, - assumedPrivilegeDetails + const role = await server.services.role.getRoleBySlug({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + selector: { + slug: req.params.slug } - }; + }); + + return { role: { ...role, projectId: role.projectId as string } }; } }); }; diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index f64d3c979f..5a6cea20fa 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { UnauthorizedError } from "@app/lib/errors"; @@ -15,7 +15,7 @@ import { ProjectSpecificPrivilegePermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ @@ -56,6 +56,10 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F if (!permissions && !privilegePermission) { throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" }); } + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug + }); const permission = privilegePermission ? privilegePermission.actions.map((action) => ({ @@ -64,19 +68,35 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F conditions: privilegePermission.conditions })) : permissions!; - const privilege = await server.services.identityProjectAdditionalPrivilege.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - ...req.body, - slug: req.body.slug ?? slugify(alphaNumericNanoId(12)), - isTemporary: false, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - permissions: backfillPermissionV1SchemaToV2Schema(permission) + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + data: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + ...req.body, + isTemporary: false, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: backfillPermissionV1SchemaToV2Schema(permission) + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -106,7 +126,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission ).optional(), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), temporaryRange: z .string() @@ -138,19 +158,39 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F })) : permissions!; - const privilege = await server.services.identityProjectAdditionalPrivilege.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - ...req.body, - slug: req.body.slug ?? slugify(alphaNumericNanoId(12)), - isTemporary: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - permissions: backfillPermissionV1SchemaToV2Schema(permission) + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug }); - return { privilege }; + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + data: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + ...req.body, + isTemporary: true, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: backfillPermissionV1SchemaToV2Schema(permission) + } + }); + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -183,7 +223,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ).optional(), isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), temporaryRange: z .string() @@ -216,18 +256,36 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F conditions: privilegePermission.conditions })) : permissions!; - const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - slug: req.body.privilegeSlug, - identityId: req.body.identityId, - projectSlug: req.body.projectSlug, + + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug + }); + + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeNameToDoc( + req.body.privilegeSlug, + projectId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.updateAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + id: privilegeDoc.id + }, data: { + ...req.body, ...updatedInfo, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts permissions: permission ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts @@ -235,7 +293,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F : undefined } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -267,16 +334,39 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilege.deleteBySlug({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - slug: req.body.privilegeSlug, - identityId: req.body.identityId, - projectSlug: req.body.projectSlug + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug }); - return { privilege }; + + const { privilegeId } = await server.services.convertor.additionalPrivilegeNameToDoc( + req.body.privilegeSlug, + projectId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.deleteAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + id: privilegeId + } + }); + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -310,15 +400,39 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsBySlug({ - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - slug: req.params.privilegeSlug, - ...req.query + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.query.projectSlug }); - return { privilege }; + + const { privilegeId } = await server.services.convertor.additionalPrivilegeNameToDoc( + req.params.privilegeSlug, + projectId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.query.identityId, + actorType: ActorType.IDENTITY, + id: privilegeId + } + }); + + return { + privilege: { + ...privilege, + identityId: req.query.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -349,15 +463,32 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privileges = await server.services.identityProjectAdditionalPrivilege.listIdentityProjectPrivileges({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.query + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.query.projectSlug }); + + const { additionalPrivileges: privileges } = await server.services.additionalPrivilege.listAdditionalPrivileges({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.query.identityId, + actorType: ActorType.IDENTITY + } + }); + return { - privileges + privileges: privileges.map((privilege) => ({ + ...privilege, + identityId: req.query.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + })) }; } }); diff --git a/backend/src/ee/routes/v1/kmip-spec-router.ts b/backend/src/ee/routes/v1/kmip-spec-router.ts index 9a1f4902ce..c7db12de9e 100644 --- a/backend/src/ee/routes/v1/kmip-spec-router.ts +++ b/backend/src/ee/routes/v1/kmip-spec-router.ts @@ -128,7 +128,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => { 200: z.object({ id: z.string(), value: z.string(), - algorithm: z.string() + algorithm: z.string(), + kmipMetadata: z.record(z.any()).optional() }) } }, @@ -433,7 +434,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => { body: z.object({ key: z.string(), name: z.string(), - algorithm: z.nativeEnum(SymmetricKeyAlgorithm) + algorithm: z.nativeEnum(SymmetricKeyAlgorithm), + kmipMetadata: z.record(z.any()).optional() }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 070462d47c..5a8f030381 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -1,7 +1,9 @@ +import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { OrgPermissionSchema } from "@app/ee/services/permission/org-permission"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -25,8 +27,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { ), name: z.string().trim(), description: z.string().trim().nullish(), - // TODO(scott): once UI refactored permissions: OrgPermissionSchema.array() - permissions: z.any().array() + permissions: OrgPermissionSchema.array() }), response: { 200: z.object({ @@ -36,13 +37,18 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.createRole( - req.permission.id, - req.params.organizationId, - req.body, - req.permission.authMethod, - req.permission.orgId - ); + const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + data: { + ...req.body, + permissions: stringifiedPermissions + } + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -59,7 +65,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -82,14 +88,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.getRole( - req.permission.id, - req.params.organizationId, - req.params.roleId, - req.permission.authMethod, - req.permission.orgId - ); - return { role }; + const role = await server.services.role.getRoleById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + selector: { + id: req.params.roleId + } + }); + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -114,8 +123,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { .optional(), name: z.string().trim().optional(), description: z.string().trim().nullish(), - // TODO(scott): once UI refactored permissions: OrgPermissionSchema.array().optional() - permissions: z.any().array().optional() + permissions: OrgPermissionSchema.array().optional() }), response: { 200: z.object({ @@ -125,14 +133,21 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.updateRole( - req.permission.id, - req.params.organizationId, - req.params.roleId, - req.body, - req.permission.authMethod, - req.permission.orgId - ); + const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + selector: { + id: req.params.roleId + }, + data: { + ...req.body, + permissions: stringifiedPermissions + } + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -149,7 +164,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -172,13 +187,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.deleteRole( - req.permission.id, - req.params.organizationId, - req.params.roleId, - req.permission.authMethod, - req.permission.orgId - ); + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + selector: { + id: req.params.roleId + } + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -189,7 +207,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -206,22 +224,26 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ data: z.object({ - roles: OrgRolesSchema.omit({ permissions: true }) - .merge(z.object({ permissions: z.unknown() })) - .array() + roles: OrgRolesSchema.omit({ permissions: true }).array() }) }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const roles = await server.services.orgRole.listRoles( - req.permission.id, - req.params.organizationId, - req.permission.authMethod, - req.permission.orgId - ); - return { data: { roles } }; + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + data: {} + }); + return { + data: { + roles: roles.map((el) => ({ ...el, orgId: el.orgId as string })) + } + }; } }); @@ -237,20 +259,33 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - membership: OrgMembershipsSchema, + memberships: z + .object({ + id: z.string(), + roles: z + .object({ + role: z.string() + }) + .array() + }) + .array(), permissions: z.any().array() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { permissions, membership } = await server.services.orgRole.getUserPermission( - req.permission.id, - req.params.organizationId, - req.permission.authMethod, - req.permission.orgId - ); - return { permissions, membership }; + const { permissions, memberships } = await server.services.role.getUserPermission({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + } + }); + return { + permissions, + memberships + }; } }); }; diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index 5a20ad893d..acf34cb3bf 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; @@ -11,7 +11,6 @@ import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ @@ -55,13 +54,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); - const role = await server.services.projectRole.createRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, data: { @@ -73,7 +70,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.CREATE_PROJECT_ROLE, metadata: { @@ -86,7 +83,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -133,12 +130,16 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; - const role = await server.services.projectRole.updateRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId, + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + }, data: { ...req.body, permissions: stringifiedPermissions @@ -148,7 +149,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.UPDATE_PROJECT_ROLE, metadata: { @@ -161,7 +162,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -192,18 +193,22 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.deleteRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + } }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.DELETE_PROJECT_ROLE, metadata: { @@ -214,7 +219,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -244,17 +249,16 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectRole.listRoles({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId - } + }, + data: {} }); - return { roles }; + return { roles: roles.map((el) => ({ ...el, projectId: el.projectId as string })) }; } }); @@ -273,24 +277,25 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - role: SanitizedRoleSchema.omit({ version: true }) + role: SanitizedRoleSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.getRoleBySlug({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.getRoleBySlug({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, - roleSlug: req.params.roleSlug + selector: { + slug: req.params.roleSlug + } }); - return { role }; + + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -307,14 +312,16 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ data: z.object({ - membership: z.object({ - id: z.string(), - roles: z - .object({ - role: z.string() - }) - .array() - }), + memberships: z + .object({ + id: z.string(), + roles: z + .object({ + role: z.string() + }) + .array() + }) + .array(), assumedPrivilegeDetails: z .object({ actorId: z.string(), @@ -330,17 +337,19 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission( - req.permission.id, - req.params.projectId, - req.permission.authMethod, - req.permission.orgId - ); + const { permissions, memberships, assumedPrivilegeDetails } = await server.services.role.getUserPermission({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: req.params.projectId, + orgId: req.permission.orgId + } + }); return { data: { permissions, - membership, + memberships, assumedPrivilegeDetails } }; diff --git a/backend/src/ee/routes/v1/relay-router.ts b/backend/src/ee/routes/v1/relay-router.ts index f3d006b108..766025c21d 100644 --- a/backend/src/ee/routes/v1/relay-router.ts +++ b/backend/src/ee/routes/v1/relay-router.ts @@ -146,4 +146,85 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => { }); } }); + + server.route({ + method: "POST", + url: "/heartbeat-instance-relay", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + name: slugSchema({ min: 1, max: 32, field: "name" }) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: (req, _, next) => { + const authHeader = req.headers.authorization; + + if (!appCfg.RELAY_AUTH_SECRET) { + throw new UnauthorizedError({ + message: "Relay authentication not configured" + }); + } + + if (!authHeader) { + throw new UnauthorizedError({ + message: "Missing authorization header" + }); + } + + const expectedHeader = `Bearer ${appCfg.RELAY_AUTH_SECRET}`; + if ( + authHeader.length === expectedHeader.length && + crypto.nativeCrypto.timingSafeEqual(Buffer.from(authHeader), Buffer.from(expectedHeader)) + ) { + return next(); + } + + throw new UnauthorizedError({ + message: "Invalid relay auth secret" + }); + }, + handler: async (req) => { + await server.services.relay.heartbeat({ + name: req.body.name + }); + + return { message: "Successfully triggered heartbeat" }; + } + }); + + server.route({ + method: "POST", + url: "/heartbeat-org-relay", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + name: slugSchema({ min: 1, max: 32, field: "name" }) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + await server.services.relay.heartbeat({ + name: req.body.name, + identityId: req.permission.id, + orgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return { message: "Successfully triggered heartbeat" }; + } + }); }; diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts index b9eeb7729c..9b7f9f35f4 100644 --- a/backend/src/ee/routes/v1/secret-router.ts +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -11,7 +11,6 @@ const AccessListEntrySchema = z .object({ allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(), id: z.string(), - membershipId: z.string(), name: z.string() }) .array(); diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index df8512adae..926b222319 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,17 +1,18 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; +import { NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/sanitizedSchema/user-additional-privilege"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ @@ -34,7 +35,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr z.object({ isTemporary: z.literal(true), temporaryMode: z - .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), temporaryRange: z .string() @@ -55,17 +56,31 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - projectMembershipId: req.body.projectMembershipId, - ...req.body.type, - slug: req.body.slug || slugify(alphaNumericNanoId(8)), - permissions: req.body.permissions + const { userId, membership } = await server.services.convertor.userMembershipIdToUserId( + req.body.projectMembershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: membership.scopeProjectId as string, + orgId: req.permission.orgId + }, + data: { + actorId: userId, + actorType: ActorType.USER, + ...req.body.type, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + permissions: req.body.permissions + } }); - return { privilege }; + + return { + privilege: { ...privilege, userId, projectId: membership.scopeProjectId as string, slug: privilege.name } + }; } }); @@ -91,7 +106,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr z.object({ isTemporary: z.literal(true).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z - .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), temporaryRange: z .string() @@ -113,21 +128,41 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.updateById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - ...req.body, - ...req.body.type, - permissions: req.body.permissions - ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - req.body.permissions - : undefined, - privilegeId: req.params.privilegeId + const data = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.privilegeId); + if (!data.privilege.actorUserId) + throw new NotFoundError({ message: `Privilege with id ${req.params.privilegeId} not found` }); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.updateAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: data.privilege.projectId as string, + orgId: req.permission.orgId + }, + data: { + ...req.body, + ...req.body.type, + permissions: req.body.permissions + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + req.body.permissions + : undefined + }, + selector: { + id: req.params.privilegeId, + actorId: data.privilege.actorUserId, + actorType: ActorType.USER + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + userId: data.privilege.actorUserId, + projectId: data.privilege.projectId as string, + slug: privilege.name + } + }; } }); @@ -149,14 +184,32 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.deleteById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - privilegeId: req.params.privilegeId + const data = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.privilegeId); + if (!data.privilege.actorUserId) + throw new NotFoundError({ message: `Privilege with id ${req.params.privilegeId} not found` }); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.deleteAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: data.privilege.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.privilegeId, + actorId: data.privilege.actorUserId, + actorType: ActorType.USER + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + userId: data.privilege.actorUserId, + projectId: data.privilege.projectId as string, + slug: privilege.name + } + }; } }); @@ -178,14 +231,33 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privileges = await server.services.projectUserAdditionalPrivilege.listPrivileges({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - projectMembershipId: req.query.projectMembershipId + const { userId, membership } = await server.services.convertor.userMembershipIdToUserId( + req.query.projectMembershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { additionalPrivileges: privileges } = await server.services.additionalPrivilege.listAdditionalPrivileges({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: membership.scopeProjectId as string, + orgId: req.permission.orgId + }, + selector: { + actorId: userId, + actorType: ActorType.USER + } }); - return { privileges }; + + return { + privileges: privileges.map((privilege) => ({ + ...privilege, + userId: membership.actorUserId as string, + projectId: membership.scopeProjectId as string, + slug: privilege.name + })) + }; } }); @@ -207,14 +279,32 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.getPrivilegeDetailsById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - privilegeId: req.params.privilegeId + const data = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.privilegeId); + if (!data.privilege.actorUserId) + throw new NotFoundError({ message: `Privilege with id ${req.params.privilegeId} not found` }); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: data.privilege.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.privilegeId, + actorId: data.privilege.actorUserId, + actorType: ActorType.USER + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + userId: data.privilege.actorUserId, + projectId: data.privilege.projectId as string, + slug: privilege.name + } + }; } }); }; diff --git a/backend/src/ee/routes/v2/deprecated-project-role-router.ts b/backend/src/ee/routes/v2/deprecated-project-role-router.ts index 326bda06a4..0a44d4d21d 100644 --- a/backend/src/ee/routes/v2/deprecated-project-role-router.ts +++ b/backend/src/ee/routes/v2/deprecated-project-role-router.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; @@ -11,7 +11,6 @@ import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ @@ -55,13 +54,11 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv handler: async (req) => { const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); - const role = await server.services.projectRole.createRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, data: { @@ -73,7 +70,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: req.params.projectId, event: { type: EventType.CREATE_PROJECT_ROLE, metadata: { @@ -86,7 +83,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -133,12 +130,16 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; - const role = await server.services.projectRole.updateRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId, + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + }, data: { ...req.body, permissions: stringifiedPermissions @@ -148,7 +149,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.UPDATE_PROJECT_ROLE, metadata: { @@ -161,7 +162,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -192,18 +193,22 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.deleteRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + } }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.DELETE_PROJECT_ROLE, metadata: { @@ -214,7 +219,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -244,17 +249,16 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectRole.listRoles({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId - } + }, + data: {} }); - return { roles }; + return { roles: roles.map((el) => ({ ...el, projectId: el.projectId as string })) }; } }); @@ -273,24 +277,25 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }), response: { 200: z.object({ - role: SanitizedRoleSchema.omit({ version: true }) + role: SanitizedRoleSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.getRoleBySlug({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.getRoleBySlug({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, - roleSlug: req.params.roleSlug + selector: { + slug: req.params.roleSlug + } }); - return { role }; + + return { role: { ...role, projectId: role.projectId as string } }; } }); }; diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index a6d4459e43..f8ac34b4b4 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; @@ -11,7 +11,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchema/identitiy-additional-privilege"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ @@ -43,7 +43,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F z.object({ isTemporary: z.literal(true), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryMode), temporaryRange: z .string() @@ -64,18 +64,31 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.create({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - projectId: req.body.projectId, - identityId: req.body.identityId, - ...req.body.type, - slug: req.body.slug || slugify(alphaNumericNanoId(8)), - permissions: req.body.permissions + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: req.body.projectId, + orgId: req.permission.orgId + }, + data: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + ...req.body.type, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + permissions: req.body.permissions + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: req.body.projectId, + projectId: req.body.projectId, + slug: privilege.name + } + }; } }); @@ -108,7 +121,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F z.object({ isTemporary: z.literal(true).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryMode), temporaryRange: z .string() @@ -129,19 +142,36 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.updateById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id, + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.id); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.updateAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: privilegeDoc.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.id, + actorId: privilegeDoc.actorIdentityId as string, + actorType: ActorType.IDENTITY + }, data: { ...req.body, ...req.body.type, permissions: req.body.permissions || undefined } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: privilegeDoc.actorIdentityId as string, + projectMembershipId: privilegeDoc.projectId as string, + projectId: privilegeDoc.projectId as string, + slug: privilege.name + } + }; } }); @@ -171,14 +201,31 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.deleteById({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - id: req.params.id + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.id); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.deleteAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: privilegeDoc.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.id, + actorId: privilegeDoc.actorIdentityId as string, + actorType: ActorType.IDENTITY + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: privilegeDoc.actorIdentityId as string, + projectMembershipId: privilegeDoc.projectId as string, + projectId: privilegeDoc.projectId as string, + slug: privilege.name + } + }; } }); @@ -208,14 +255,31 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsById({ - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - id: req.params.id + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.id); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: privilegeDoc.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.id, + actorId: privilegeDoc.actorIdentityId as string, + actorType: ActorType.IDENTITY + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: privilegeDoc.actorIdentityId as string, + projectMembershipId: privilegeDoc.projectId as string, + projectId: privilegeDoc.projectId as string, + slug: privilege.name + } + }; } }); @@ -249,15 +313,36 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsBySlug({ - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - slug: req.params.privilegeSlug, - ...req.query + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.query.projectSlug, + orgId: req.permission.orgId }); - return { privilege }; + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeByName( + { + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + name: req.params.privilegeSlug, + actorId: req.query.identityId, + actorType: ActorType.IDENTITY + } + } + ); + + return { + privilege: { + ...privilege, + identityId: req.query.identityId, + projectMembershipId: privilege.projectId as string, + projectId, + slug: privilege.name + } + }; } }); @@ -288,15 +373,27 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privileges = await server.services.identityProjectAdditionalPrivilegeV2.listIdentityProjectPrivileges({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.query + const { additionalPrivileges: privileges } = await server.services.additionalPrivilege.listAdditionalPrivileges({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: req.query.projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.query.identityId, + actorType: ActorType.IDENTITY + } }); + return { - privileges + privileges: privileges.map((privilege) => ({ + ...privilege, + identityId: req.query.identityId, + projectMembershipId: privilege.projectId as string, + projectId: req.query.projectId, + slug: privilege.name + })) }; } }); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 95d1a98779..16c7e962c7 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -1,21 +1,20 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { AccessScope, ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; +import { TMembershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal"; import { TAccessApprovalRequestReviewerDALFactory } from "../access-approval-request/access-approval-request-reviewer-dal"; import { ApprovalStatus } from "../access-approval-request/access-approval-request-types"; import { TGroupDALFactory } from "../group/group-dal"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TAccessApprovalPolicyApproverDALFactory, TAccessApprovalPolicyBypasserDALFactory @@ -39,14 +38,13 @@ type TAccessApprovalPolicyServiceFactoryDep = { projectEnvDAL: Pick; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; accessApprovalPolicyBypasserDAL: TAccessApprovalPolicyBypasserDALFactory; - projectMembershipDAL: Pick; groupDAL: TGroupDALFactory; userDAL: Pick; accessApprovalRequestDAL: Pick; - additionalPrivilegeDAL: Pick; + additionalPrivilegeDAL: Pick; accessApprovalRequestReviewerDAL: Pick; - orgMembershipDAL: Pick; accessApprovalPolicyEnvironmentDAL: TAccessApprovalPolicyEnvironmentDALFactory; + membershipUserDAL: TMembershipUserDALFactory; }; export const accessApprovalPolicyServiceFactory = ({ @@ -62,7 +60,7 @@ export const accessApprovalPolicyServiceFactory = ({ accessApprovalRequestDAL, additionalPrivilegeDAL, accessApprovalRequestReviewerDAL, - orgMembershipDAL + membershipUserDAL }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { const $policyExists = async ({ envId, @@ -424,13 +422,14 @@ export const accessApprovalPolicyServiceFactory = ({ // Validate user bypassers if (bypasserUserIds.length > 0) { - const orgMemberships = await orgMembershipDAL.find({ - $in: { userId: bypasserUserIds }, - orgId: actorOrgId + const orgMemberships = await membershipUserDAL.find({ + $in: { actorUserId: bypasserUserIds }, + scopeOrgId: actorOrgId, + scope: AccessScope.Organization }); if (orgMemberships.length !== bypasserUserIds.length) { - const foundUserIdsInOrg = new Set(orgMemberships.map((mem) => mem.userId)); + const foundUserIdsInOrg = new Set(orgMemberships.map((mem) => mem.actorUserId as string)); const missingUserIds = bypasserUserIds.filter((id) => !foundUserIdsInOrg.has(id)); throw new BadRequestError({ message: `One or more specified bypasser users are not part of the organization or do not exist. Invalid or non-member user IDs: ${missingUserIds.join(", ")}` @@ -633,7 +632,7 @@ export const accessApprovalPolicyServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -641,9 +640,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` }); 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 f3972fc1cc..3bea66696d 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 @@ -3,9 +3,10 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { AccessApprovalRequestsSchema, + AccessScope, TableName, TAccessApprovalRequests, - TOrgMemberships, + TMemberships, TUserGroupMembership, TUsers } from "@app/db/schemas"; @@ -244,11 +245,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR const docs = await db .replicaNode()(TableName.AccessApprovalRequest) .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) - .leftJoin( - TableName.ProjectUserAdditionalPrivilege, + TableName.AdditionalPrivilege, `${TableName.AccessApprovalRequest}.privilegeId`, - `${TableName.ProjectUserAdditionalPrivilege}.id` + `${TableName.AdditionalPrivilege}.id` ) .leftJoin( TableName.AccessApprovalPolicy, @@ -276,7 +276,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR `${TableName.UserGroupMembership}.groupId` ) .leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .leftJoin( TableName.AccessApprovalPolicyBypasser, `${TableName.AccessApprovalPolicy}.id`, @@ -294,24 +293,24 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR `requestedByUser.id` ) - .leftJoin( - db(TableName.OrgMembership).as("approverOrgMembership"), - `${TableName.AccessApprovalPolicyApprover}.approverUserId`, - `approverOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("approverGroupOrgMembership"), - `${TableName.Users}.id`, - `approverGroupOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("reviewerOrgMembership"), - `${TableName.AccessApprovalRequestReviewer}.reviewerUserId`, - `reviewerOrgMembership.userId` - ) - + .leftJoin(db(TableName.Membership).as("approverOrgMembership"), (qb) => { + qb.on( + `${TableName.AccessApprovalPolicyApprover}.approverUserId`, + `approverOrgMembership.actorUserId` + ).andOn(`approverOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) + .leftJoin(db(TableName.Membership).as("approverGroupOrgMembership"), (qb) => { + qb.on(`${TableName.Users}.id`, `approverGroupOrgMembership.actorUserId`).andOn( + `approverGroupOrgMembership.scope`, + db.raw("?", [AccessScope.Organization]) + ); + }) + .leftJoin(db(TableName.Membership).as("reviewerOrgMembership"), (qb) => { + qb.on( + `${TableName.AccessApprovalRequestReviewer}.reviewerUserId`, + `reviewerOrgMembership.actorUserId` + ).andOn(`reviewerOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .select(selectAllTableCols(TableName.AccessApprovalRequest)) @@ -360,22 +359,22 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"), - db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"), + db.ref("actorUserId").withSchema(TableName.AdditionalPrivilege).as("privilegeUserId"), + db.ref("projectId").withSchema(TableName.AdditionalPrivilege).as("privilegeMembershipId"), - db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), - db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("privilegeIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("privilegeTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("privilegeTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("privilegeTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("privilegeTemporaryAccessEndTime"), - db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("privilegePermissions") ) .orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc"); @@ -408,7 +407,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR privilege: doc.privilegeId ? { membershipId: doc.privilegeMembershipId, - userId: doc.privilegeUserId, + userId: doc.privilegeUserId || "", projectId: doc.projectId, isTemporary: doc.privilegeIsTemporary, temporaryMode: doc.privilegeTemporaryMode, @@ -773,9 +772,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR ) .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .leftJoin( - TableName.ProjectUserAdditionalPrivilege, + TableName.AdditionalPrivilege, `${TableName.AccessApprovalRequest}.privilegeId`, - `${TableName.ProjectUserAdditionalPrivilege}.id` + `${TableName.AdditionalPrivilege}.id` ) .leftJoin( diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index d69c6da790..1027995a7d 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import msFn from "ms"; -import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, TemporaryPermissionMode } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -10,12 +10,12 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { EnforcementLevel } from "@app/lib/types"; import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; import { TriggerFeature } from "@app/lib/workflow-integrations/types"; +import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -26,15 +26,13 @@ import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-poli import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TGroupDALFactory } from "../group/group-dal"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; -import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { verifyRequestedPermissions } from "./access-approval-request-fns"; import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; import { ApprovalStatus, TAccessApprovalRequestServiceFactory } from "./access-approval-request-types"; type TSecretApprovalRequestServiceFactoryDep = { - additionalPrivilegeDAL: Pick; + additionalPrivilegeDAL: Pick; permissionService: Pick; accessApprovalPolicyApproverDAL: Pick; projectEnvDAL: Pick; @@ -59,7 +57,6 @@ type TSecretApprovalRequestServiceFactoryDep = { "create" | "find" | "findOne" | "transaction" | "delete" >; groupDAL: Pick; - projectMembershipDAL: Pick; smtpService: Pick; userDAL: Pick< TUserDALFactory, @@ -125,7 +122,7 @@ export const accessApprovalRequestServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); // Anyone can create an access approval request. - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -133,9 +130,6 @@ export const accessApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const requestedByUser = await userDAL.findById(actorId); if (!requestedByUser) throw new ForbiddenRequestError({ message: "User not found" }); @@ -340,7 +334,7 @@ export const accessApprovalRequestServiceFactory = ({ }); } - const { membership, hasRole } = await permissionService.getProjectPermission({ + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId: accessApprovalRequest.projectId, @@ -349,10 +343,6 @@ export const accessApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } - const isApprover = policy.approvers.find((approver) => approver.userId === actorId); if (!hasRole(ProjectMembershipRole.Admin) && !isApprover) { @@ -496,7 +486,7 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -504,9 +494,6 @@ export const accessApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); @@ -566,7 +553,7 @@ export const accessApprovalRequestServiceFactory = ({ slug: permissionEnvironment }); - const { membership, hasRole } = await permissionService.getProjectPermission({ + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId: accessApprovalRequest.projectId, @@ -575,10 +562,6 @@ export const accessApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } - const isSelfApproval = actorId === accessApprovalRequest.requestedByUserId; const isSoftEnforcement = policy.enforcementLevel === EnforcementLevel.Soft; const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId); @@ -724,9 +707,9 @@ export const accessApprovalRequestServiceFactory = ({ // Permanent access const privilege = await additionalPrivilegeDAL.create( { - userId: accessApprovalRequest.requestedByUserId, + actorUserId: accessApprovalRequest.requestedByUserId, projectId: accessApprovalRequest.projectId, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + name: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions) }, tx @@ -739,12 +722,12 @@ export const accessApprovalRequestServiceFactory = ({ const privilege = await additionalPrivilegeDAL.create( { - userId: accessApprovalRequest.requestedByUserId, + actorUserId: accessApprovalRequest.requestedByUserId, projectId: accessApprovalRequest.projectId, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + name: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions), isTemporary: true, // Explicitly set to true for the privilege - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryMode: TemporaryPermissionMode.Relative, temporaryRange: accessApprovalRequest.temporaryRange!, temporaryAccessStartTime: startTime, temporaryAccessEndTime: new Date(startTime.getTime() + relativeTempAllocatedTimeInMs) @@ -830,7 +813,7 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -838,9 +821,6 @@ export const accessApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const count = await accessApprovalRequestDAL.getCount({ projectId: project.id, policyId }); diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index f4d43f23fc..1bcfa39385 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -34,6 +34,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const $getClient = async (providerInputs: z.infer) => { const connection = new ElasticSearchClient({ + requestTimeout: 30_000, node: { url: new URL(`${providerInputs.host}:${providerInputs.port}`), ...(providerInputs.ca && { diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts index da9d3c1ef6..1896192cd2 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts @@ -10,20 +10,34 @@ export type TGatewayV2DALFactory = ReturnType; export const gatewayV2DalFactory = (db: TDbClient) => { const orm = ormify(db, TableName.GatewayV2); - const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + const find = async ( + filter: TFindFilter & { isHeartbeatStale?: boolean }, + { offset, limit, sort, tx }: TFindOpt = {} + ) => { try { + const { isHeartbeatStale, ...regularFilter } = filter; + const query = (tx || db.replicaNode())(TableName.GatewayV2) // eslint-disable-next-line @typescript-eslint/no-misused-promises - .where(buildFindFilter(filter, TableName.GatewayV2)) + .where(buildFindFilter(regularFilter, TableName.GatewayV2)) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.GatewayV2}.identityId`) - .join( - TableName.IdentityOrgMembership, - `${TableName.IdentityOrgMembership}.identityId`, - `${TableName.GatewayV2}.identityId` - ) .select(selectAllTableCols(TableName.GatewayV2)) .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); + if (isHeartbeatStale) { + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + void query.where(`${TableName.GatewayV2}.heartbeat`, "<", oneHourAgo); + void query.where((v) => { + void v + .whereNull(`${TableName.GatewayV2}.healthAlertedAt`) + .orWhere( + `${TableName.GatewayV2}.healthAlertedAt`, + "<", + db.ref("heartbeat").withSchema(TableName.GatewayV2) + ); + }); + } + if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index 4bf6b1aef2..dce5bc2990 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -2,14 +2,17 @@ import net from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { CronJob } from "cron"; -import { TRelays } from "@app/db/schemas"; +import { OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; import { GatewayProxyProtocol } from "@app/lib/gateway/types"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; +import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; @@ -20,6 +23,10 @@ import { } from "@app/services/certificate-authority/certificate-authority-fns"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TLicenseServiceFactory } from "../license/license-service"; import { PamResource } from "../pam-resource/pam-resource-enums"; @@ -39,6 +46,9 @@ type TGatewayV2ServiceFactoryDep = { gatewayV2DAL: TGatewayV2DALFactory; relayDAL: TRelayDALFactory; permissionService: TPermissionServiceFactory; + orgDAL: Pick; + notificationService: Pick; + smtpService: Pick; }; export type TGatewayV2ServiceFactory = ReturnType; @@ -50,7 +60,10 @@ export const gatewayV2ServiceFactory = ({ relayService, gatewayV2DAL, relayDAL, - permissionService + permissionService, + orgDAL, + notificationService, + smtpService }: TGatewayV2ServiceFactoryDep) => { const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { const orgLicensePlan = await licenseService.getPlan(orgId); @@ -878,6 +891,72 @@ export const gatewayV2ServiceFactory = ({ }); }; + const $healthcheckNotify = async () => { + const unhealthyGateways = await gatewayV2DAL.find({ + isHeartbeatStale: true + }); + + if (unhealthyGateways.length === 0) return; + + logger.warn( + { gatewayIds: unhealthyGateways.map((g) => g.id) }, + "Found gateways with last heartbeat over an hour ago. Sending notifications." + ); + + const gatewaysByOrg = groupBy(unhealthyGateways, (gw) => gw.orgId); + + for await (const [orgId, gateways] of Object.entries(gatewaysByOrg)) { + try { + const admins = await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin); + if (admins.length === 0) { + logger.warn({ orgId }, "Organization has no admins to notify about unhealthy gateway."); + // eslint-disable-next-line no-continue + continue; + } + + const gatewayNames = gateways.map((g) => `"${g.name}"`).join(", "); + const body = `The following gateway(s) in your organization may be offline as they haven't reported a heartbeat in over an hour: ${gatewayNames}. Please check their status.`; + + await notificationService.createUserNotifications( + admins.map((admin) => ({ + userId: admin.user.id, + orgId, + type: NotificationType.GATEWAY_HEALTH_ALERT, + title: "Gateway Health Alert", + body, + link: "/organization/networking" + })) + ); + + await smtpService.sendMail({ + recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v), + subjectLine: "Gateway Health Alert", + substitutions: { + type: "gateway", + names: gatewayNames + }, + template: SmtpTemplates.HealthAlert + }); + + await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() }))); + } catch (error) { + logger.error(error, `Failed to send gateway health notifications for organization [orgId=${orgId}]`); + } + } + }; + + const initializeHealthcheckNotify = async () => { + logger.info("Setting up background notification process for gateway v2 health-checks"); + + await $healthcheckNotify(); + + // run every 5 minutes + const job = new CronJob("*/5 * * * *", $healthcheckNotify); + job.start(); + + return job; + }; + return { listGateways, registerGateway, @@ -885,6 +964,7 @@ export const gatewayV2ServiceFactory = ({ getPAMConnectionDetails, deleteGatewayById, heartbeat, - getPamSessionKey + getPamSessionKey, + initializeHealthcheckNotify }; }; diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts index c21ff31c01..58f59d3139 100644 --- a/backend/src/ee/services/gateway/gateway-dal.ts +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -1,5 +1,5 @@ import { TDbClient } from "@app/db"; -import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; +import { AccessScope, GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; @@ -17,17 +17,14 @@ export const gatewayDALFactory = (db: TDbClient) => { // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter, TableName.Gateway, ["orgId"])) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) - .join( - TableName.IdentityOrgMembership, - `${TableName.IdentityOrgMembership}.identityId`, - `${TableName.Gateway}.identityId` - ) + .join(TableName.Membership, `${TableName.Membership}.actorIdentityId`, `${TableName.Gateway}.identityId`) .select(selectAllTableCols(TableName.Gateway)) - .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId")) - .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); + .select(db.ref("scopeOrgId").withSchema(TableName.Membership).as("identityOrgId")) + .select(db.ref("name").withSchema(TableName.Identity).as("identityName")) + .where(`${TableName.Membership}.scope`, AccessScope.Organization); if (filter.orgId) { - void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId); + void query.where(`${TableName.Membership}.scopeOrgId`, filter.orgId); } if (limit) void query.limit(limit); if (offset) void query.offset(offset); @@ -39,7 +36,7 @@ export const gatewayDALFactory = (db: TDbClient) => { return docs.map((el) => ({ ...GatewaysSchema.parse(el), - orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string + orgId: el.identityOrgId, identity: { id: el.identityId, name: el.identityName } })); } catch (error) { diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts index 7c4ad15eb4..b2bcb4ef3d 100644 --- a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -6,13 +6,15 @@ import { paginateGraphql } from "@octokit/plugin-paginate-graphql"; import { Octokit as OctokitRest } from "@octokit/rest"; import RE2 from "re2"; -import { OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { retryWithBackoff } from "@app/lib/retry"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TGroupDALFactory } from "../group/group-dal"; @@ -77,11 +79,10 @@ type TGithubOrgSyncServiceFactoryDep = { "findGroupMembershipsByUserIdInOrg" | "findGroupMembershipsByGroupIdInOrg" | "insertMany" | "delete" >; groupDAL: Pick; + membershipRoleDAL: Pick; + membershipGroupDAL: Pick; licenseService: Pick; - orgMembershipDAL: Pick< - TOrgMembershipDALFactory, - "find" | "findOrgMembershipById" | "findOrgMembershipsWithUsersByOrgId" - >; + orgMembershipDAL: Pick; }; export type TGithubOrgSyncServiceFactory = ReturnType; @@ -93,7 +94,9 @@ export const githubOrgSyncServiceFactory = ({ userGroupMembershipDAL, groupDAL, licenseService, - orgMembershipDAL + orgMembershipDAL, + membershipRoleDAL, + membershipGroupDAL }: TGithubOrgSyncServiceFactoryDep) => { const createGithubOrgSync = async ({ githubOrgName, @@ -368,6 +371,23 @@ export const githubOrgSyncServiceFactory = ({ })), tx ); + const memberships = await membershipGroupDAL.insertMany( + newGroups.map((el) => ({ + actorGroupId: el.id, + scope: AccessScope.Organization, + scopeOrgId: orgId + })), + tx + ); + + await membershipRoleDAL.insertMany( + memberships.map((el) => ({ + membershipId: el.id, + role: OrgMembershipRole.Member + })), + tx + ); + await userGroupMembershipDAL.insertMany( newGroups.map((el) => ({ groupId: el.id, @@ -694,6 +714,23 @@ export const githubOrgSyncServiceFactory = ({ tx ); + const memberships = await membershipGroupDAL.insertMany( + newGroups.map((el) => ({ + actorGroupId: el.id, + scope: AccessScope.Organization, + scopeOrgId: orgPermission.orgId + })), + tx + ); + + await membershipRoleDAL.insertMany( + memberships.map((el) => ({ + membershipId: el.id, + role: OrgMembershipRole.Member + })), + tx + ); + newGroups.forEach((group) => { if (!existingTeamsMap[group.name]) { existingTeamsMap[group.name] = []; diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts index 708fbdbd36..6fb02207da 100644 --- a/backend/src/ee/services/group/group-dal.ts +++ b/backend/src/ee/services/group/group-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TGroups } from "@app/db/schemas"; +import { AccessScope, TableName, TGroups } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; @@ -20,7 +20,7 @@ export const groupDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.Groups)); if (limit) void query.limit(limit); - if (offset) void query.limit(offset); + if (offset && offset > 0) void query.offset(offset); if (sort) { void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); } @@ -36,14 +36,21 @@ export const groupDALFactory = (db: TDbClient) => { try { const docs = await (tx || db.replicaNode())(TableName.Groups) .where(`${TableName.Groups}.orgId`, orgId) - .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .join(TableName.Membership, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select(selectAllTableCols(TableName.Groups)) // cr stands for custom role - .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) - .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) - .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles)) - .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)); + .select(db.ref("id").as("crId").withSchema(TableName.Role)) + .select(db.ref("name").as("crName").withSchema(TableName.Role)) + .select(db.ref("role").withSchema(TableName.MembershipRole)) + .select(db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole)) + .select(db.ref("slug").as("crSlug").withSchema(TableName.Role)) + .select(db.ref("description").as("crDescription").withSchema(TableName.Role)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.Role)); + return docs.map(({ crId, crDescription, crSlug, crPermission, crName, ...el }) => ({ ...el, customRole: el.roleId @@ -81,9 +88,11 @@ export const groupDALFactory = (db: TDbClient) => { }) => { try { const query = db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .leftJoin(TableName.UserGroupMembership, (bd) => { bd.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn( `${TableName.UserGroupMembership}.groupId`, @@ -92,7 +101,7 @@ export const groupDALFactory = (db: TDbClient) => { ); }) .select( - db.ref("id").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("createdAt").withSchema(TableName.UserGroupMembership).as("joinedGroupAt"), db.ref("email").withSchema(TableName.Users), @@ -160,8 +169,10 @@ export const groupDALFactory = (db: TDbClient) => { const findGroupsByProjectId = async (projectId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.Groups) - .join(TableName.GroupProjectMembership, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + .join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorGroupId`) .select(selectAllTableCols(TableName.Groups)); return docs; } catch (error) { @@ -172,11 +183,16 @@ export const groupDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { const doc = await (tx || db.replicaNode())(TableName.Groups) - .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) + .join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .where(`${TableName.Groups}.id`, id) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) .select( selectAllTableCols(TableName.Groups), - db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles) + db.ref("slug").as("customRoleSlug").withSchema(TableName.Role), + db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole), + db.ref("role").withSchema(TableName.MembershipRole) ) .first(); @@ -186,12 +202,36 @@ export const groupDALFactory = (db: TDbClient) => { } }; + const findOne = async (filter: Partial, tx?: Knex): Promise => { + try { + const doc = await (tx || db.replicaNode())(TableName.Groups) + .join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where((queryBuilder) => { + Object.entries(filter).forEach(([key, value]) => { + void queryBuilder.where(`${TableName.Groups}.${key}`, value); + }); + }) + .select( + selectAllTableCols(TableName.Groups), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole) + ) + .first(); + return doc; + } catch (error) { + throw new DatabaseError({ error, name: "Find one" }); + } + }; + return { ...groupOrm, findGroups, findByOrgId, findAllGroupPossibleMembers, findGroupsByProjectId, - findById + findById, + findOne }; }; diff --git a/backend/src/ee/services/group/group-fns.ts b/backend/src/ee/services/group/group-fns.ts index 56f8df6c07..c4384cc4e2 100644 --- a/backend/src/ee/services/group/group-fns.ts +++ b/backend/src/ee/services/group/group-fns.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; @@ -16,7 +16,7 @@ const addAcceptedUsersToGroup = async ({ group, userGroupMembershipDAL, userDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -42,13 +42,15 @@ const addAcceptedUsersToGroup = async ({ const projectIds = Array.from( new Set( ( - await groupProjectDAL.find( + await membershipGroupDAL.find( { - groupId: group.id + actorGroupId: group.id, + scopeOrgId: group.orgId, + scope: AccessScope.Project }, { tx } ) - ).map((gp) => gp.projectId) + ).map((gp) => gp.scopeProjectId as string) ) ); @@ -167,11 +169,11 @@ export const addUsersToGroupByUserIds = async ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TAddUsersToGroupByUserIds) => { const processAddition = async (tx: Knex) => { const foundMembers = await userDAL.find( @@ -214,15 +216,18 @@ export const addUsersToGroupByUserIds = async ({ // check if all user(s) are part of the organization const existingUserOrgMemberships = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: group.orgId, + scope: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: userIds + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userIds } }, { tx } ); - const existingUserOrgMembershipsUserIdsSet = new Set(existingUserOrgMemberships.map((u) => u.userId)); + const existingUserOrgMembershipsUserIdsSet = new Set( + existingUserOrgMemberships.map((u) => u.actorUserId as string) + ); userIds.forEach((userId) => { if (!existingUserOrgMembershipsUserIdsSet.has(userId)) @@ -250,7 +255,7 @@ export const addUsersToGroupByUserIds = async ({ group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -292,9 +297,9 @@ export const removeUsersFromGroupByUserIds = async ({ userIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TRemoveUsersFromGroupByUserIds) => { const processRemoval = async (tx: Knex) => { const foundMembers = await userDAL.find({ @@ -352,13 +357,15 @@ export const removeUsersFromGroupByUserIds = async ({ const projectIds = Array.from( new Set( ( - await groupProjectDAL.find( + await membershipGroupDAL.find( { - groupId: group.id + scope: AccessScope.Project, + actorGroupId: group.id, + scopeOrgId: group.orgId }, { tx } ) - ).map((gp) => gp.projectId) + ).map((gp) => gp.scopeProjectId as string) ) ); @@ -422,11 +429,11 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({ userIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TConvertPendingGroupAdditionsToGroupMemberships) => { const processConversion = async (tx: Knex) => { const users = await userDAL.find( @@ -463,7 +470,7 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({ group: pendingGroupAddition.group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index d4d30b5ae9..075488488b 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -1,11 +1,12 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -35,8 +36,9 @@ type TGroupServiceFactoryDep = { TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction" >; - groupProjectDAL: Pick; - orgDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; + orgDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" @@ -46,7 +48,7 @@ type TGroupServiceFactoryDep = { projectKeyDAL: Pick; permissionService: Pick< TPermissionServiceFactory, - "getOrgPermission" | "getOrgPermissionByRole" | "invalidateProjectPermissionCache" + "getOrgPermission" | "getOrgPermissionByRoles" | "invalidateProjectPermissionCache" >; licenseService: Pick; oidcConfigDAL: Pick; @@ -57,7 +59,6 @@ export type TGroupServiceFactory = ReturnType; export const groupServiceFactory = ({ userDAL, groupDAL, - groupProjectDAL, orgDAL, userGroupMembershipDAL, projectDAL, @@ -65,12 +66,14 @@ export const groupServiceFactory = ({ projectKeyDAL, permissionService, licenseService, - oidcConfigDAL + oidcConfigDAL, + membershipGroupDAL, + membershipRoleDAL }: TGroupServiceFactoryDep) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -85,25 +88,23 @@ export const groupServiceFactory = ({ message: "Failed to create group due to plan restriction. Upgrade plan to create group." }); - const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( - role, - actorOrgId - ); - const isCustomRole = Boolean(customRole); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); if (role !== OrgMembershipRole.NoAccess) { const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), @@ -125,7 +126,25 @@ export const groupServiceFactory = ({ slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), orgId: actorOrgId, role: isCustomRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + roleId: null + }, + tx + ); + + const membership = await membershipGroupDAL.create( + { + actorGroupId: newGroup.id, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }, + tx + ); + + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: isCustomRole ? OrgMembershipRole.Custom : role, + customRoleId: rolePermissionDetails?.role?.id }, tx ); @@ -148,7 +167,7 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -169,32 +188,31 @@ export const groupServiceFactory = ({ throw new NotFoundError({ message: `Failed to find group with ID ${id}` }); } - let customRole: TOrgRoles | undefined; + let customRole: TRoles | undefined; if (role) { - const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole( - role, - group.orgId - ); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], group.orgId); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); - const isCustomRole = Boolean(customOrgRole); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), details: { missingPermissions: permissionBoundary.missingPermissions } }); - if (isCustomRole) customRole = customOrgRole; + if (isCustomRole) customRole = rolePermissionDetails?.role; } const updatedGroup = await groupDAL.transaction(async (tx) => { @@ -208,35 +226,44 @@ export const groupServiceFactory = ({ } } - const [updated] = await groupDAL.update( - { - id: group.id - }, - { - name, - slug: slug ? slugify(slug) : undefined, - ...(role - ? { - role: customRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id ?? null - } - : {}) - }, - tx - ); + let updated = group; + + if (name || slug) { + [updated] = await groupDAL.update( + { + id: group.id + }, + { + name, + slug: slug ? slugify(slug) : undefined + }, + tx + ); + } + + if (role) { + const membership = await membershipGroupDAL.findOne( + { + scope: AccessScope.Organization, + actorGroupId: updated.id, + scopeOrgId: updated.orgId + }, + tx + ); + await membershipRoleDAL.delete({ membershipId: membership.id }, tx); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: customRole ? OrgMembershipRole.Custom : role, + customRoleId: customRole?.id ?? null + }, + tx + ); + } return updated; }); - if (role) { - const groupProjects = await groupProjectDAL.find({ groupId: group.id }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => - permissionService.invalidateProjectPermissionCache(groupProject.projectId) - ) - ]); - } - return updatedGroup; }; @@ -259,17 +286,11 @@ export const groupServiceFactory = ({ message: "Failed to delete group due to plan restriction. Upgrade plan to delete group." }); - const groupProjects = await groupProjectDAL.find({ groupId: id }); - const [group] = await groupDAL.delete({ id, orgId: actorOrgId }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) - ]); - return group; }; @@ -344,7 +365,7 @@ export const groupServiceFactory = ({ const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -376,22 +397,23 @@ export const groupServiceFactory = ({ }); } - const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups, permission, - groupRolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to add user to more privileged group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups ), @@ -410,17 +432,12 @@ export const groupServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL }); - const groupProjects = await groupProjectDAL.find({ groupId: group.id }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) - ]); - return users[0]; }; @@ -434,7 +451,7 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -466,21 +483,22 @@ export const groupServiceFactory = ({ }); } - const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups, permission, - groupRolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to delete user from more privileged group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups ), @@ -498,15 +516,10 @@ export const groupServiceFactory = ({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL }); - const groupProjects = await groupProjectDAL.find({ groupId: group.id }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) - ]); - return users[0]; }; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index e91f7a47b4..4b07422018 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -3,7 +3,7 @@ import { Knex } from "knex"; import { TGroups } from "@app/db/schemas"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TGenericPermission } from "@app/lib/types"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -63,7 +63,7 @@ export type TAddUsersToGroup = { group: TGroups; userDAL: Pick; userGroupMembershipDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -76,7 +76,7 @@ export type TAddUsersToGroupByUserIds = { userDAL: Pick; userGroupMembershipDAL: Pick; orgDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -88,7 +88,7 @@ export type TRemoveUsersFromGroupByUserIds = { userIds: string[]; userDAL: Pick; userGroupMembershipDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; tx?: Knex; }; @@ -100,7 +100,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = { TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds" >; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; 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 374459b0c9..886eebe29e 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; @@ -18,21 +18,19 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { */ const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[], tx?: Knex) => { try { - const userProjectMemberships: string[] = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where(`${TableName.ProjectMembership}.userId`, userId) - .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds) - .pluck(`${TableName.ProjectMembership}.projectId`); + const userProjectMemberships: string[] = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.actorUserId`, userId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereIn(`${TableName.Membership}.scopeProjectId`, projectIds) + .pluck(`${TableName.Membership}.scopeProjectId`); const userGroupMemberships: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.userId`, userId) .whereNot(`${TableName.UserGroupMembership}.groupId`, groupId) - .join( - TableName.GroupProjectMembership, - `${TableName.UserGroupMembership}.groupId`, - `${TableName.GroupProjectMembership}.groupId` - ) - .whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds) - .pluck(`${TableName.GroupProjectMembership}.projectId`); + .join(TableName.Membership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereIn(`${TableName.Membership}.scopeProjectId`, projectIds) + .pluck(`${TableName.Membership}.scopeProjectId`); return new Set(userProjectMemberships.concat(userGroupMemberships)); } catch (error) { @@ -44,13 +42,10 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string, tx?: Knex) => { try { const usernameDocs: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership) - .join( - TableName.GroupProjectMembership, - `${TableName.UserGroupMembership}.groupId`, - `${TableName.GroupProjectMembership}.groupId` - ) + .join(TableName.Membership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + .where(`${TableName.Membership}.scopeProjectId`, projectId) .whereIn(`${TableName.Users}.username`, usernames) .pluck(`${TableName.Users}.id`); @@ -73,24 +68,25 @@ 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.replicaNode())(TableName.GroupProjectMembership) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) - .whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId) - .pluck(`${TableName.GroupProjectMembership}.groupId`); + const groups: string[] = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .whereNot(`${TableName.Membership}.actorGroupId`, groupId) + .pluck(`${TableName.Membership}.actorGroupId`); // main query const members = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.groupId`, groupId) .where(`${TableName.UserGroupMembership}.isPending`, false) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.ProjectMembership, (bd) => { - bd.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn( - `${TableName.ProjectMembership}.projectId`, + .leftJoin(TableName.Membership, (bd) => { + bd.on(`${TableName.Users}.id`, "=", `${TableName.Membership}.actorUserId`).andOn( + `${TableName.Membership}.scopeProjectId`, "=", db.raw("?", [projectId]) ); }) - .whereNull(`${TableName.ProjectMembership}.userId`) + .whereNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, @@ -166,15 +162,17 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { const docs = await db .replicaNode()(TableName.UserGroupMembership) .join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) - .join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`) + .join(TableName.Membership, `${TableName.UserGroupMembership}.userId`, `${TableName.Membership}.actorUserId`) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .where(`${TableName.UserGroupMembership}.userId`, userId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) .where(`${TableName.Groups}.orgId`, orgId) .select( db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("name").withSchema(TableName.Groups).as("groupName"), - db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"), + db.ref("id").withSchema(TableName.Membership).as("orgMembershipId"), db.ref("firstName").withSchema(TableName.Users).as("firstName"), db.ref("lastName").withSchema(TableName.Users).as("lastName"), db.ref("slug").withSchema(TableName.Groups).as("groupSlug") @@ -191,15 +189,17 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { const docs = await db .replicaNode()(TableName.UserGroupMembership) .join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) - .join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`) + .join(TableName.Membership, `${TableName.UserGroupMembership}.userId`, `${TableName.Membership}.actorUserId`) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .where(`${TableName.Groups}.id`, groupId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) .where(`${TableName.Groups}.orgId`, orgId) .select( db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("name").withSchema(TableName.Groups).as("groupName"), - db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"), + db.ref("id").withSchema(TableName.Membership).as("orgMembershipId"), db.ref("firstName").withSchema(TableName.Users).as("firstName"), db.ref("lastName").withSchema(TableName.Users).as("lastName") ); diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts deleted file mode 100644 index a7d8794a4d..0000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectAdditionalPrivilegeV2DALFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeV2DALFactory ->; - -export const identityProjectAdditionalPrivilegeV2DALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts deleted file mode 100644 index 27b67367e0..0000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { ForbiddenError, subject } from "@casl/ability"; -import { packRules } from "@casl/ability/extra"; - -import { ActionProjectType, TableName } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; - -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; -import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; -import { - IdentityProjectAdditionalPrivilegeTemporaryMode, - TCreateIdentityPrivilegeDTO, - TDeleteIdentityPrivilegeByIdDTO, - TGetIdentityPrivilegeDetailsByIdDTO, - TGetIdentityPrivilegeDetailsBySlugDTO, - TListIdentityPrivilegesDTO, - TUpdateIdentityPrivilegeByIdDTO -} from "./identity-project-additional-privilege-v2-types"; - -type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = { - identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; - identityProjectDAL: Pick; - projectDAL: Pick; - permissionService: Pick; -}; - -export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeV2ServiceFactory ->; - -export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ - identityProjectAdditionalPrivilegeDAL, - identityProjectDAL, - projectDAL, - permissionService -}: TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep) => { - const create = async ({ - slug, - actor, - actorId, - projectId, - actorOrgId, - identityId, - permissions: customPermission, - actorAuthMethod, - ...dto - }: TCreateIdentityPrivilegeDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug) throw new BadRequestError({ message: "Additional privilege with provided slug already exists" }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateById = async ({ - id, - data, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TUpdateIdentityPrivilegeByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - if (data?.slug) { - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug: data.slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug && existingSlug.id !== identityPrivilege.id) - throw new BadRequestError({ message: "Additional privilege with provided slug already exists" }); - } - - const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; - const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; - if (isTemporary) { - const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; - const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: data.isTemporary, - temporaryRange: data.temporaryRange, - temporaryMode: data.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteById = async ({ actorId, id, actor, actorOrgId, actorAuthMethod }: TDeleteIdentityPrivilegeByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - const { permission: identityRolePermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - identityRolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsById = async ({ - id, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsBySlug = async ({ - identityId, - slug, - projectSlug, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsBySlugDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug ${slug} not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" }); - - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const listIdentityProjectPrivileges = async ({ - identityId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - projectId - }: TListIdentityPrivilegesDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find( - { - projectMembershipId: identityProjectMembership.id - }, - { sort: [[`${TableName.IdentityProjectAdditionalPrivilege}.slug` as "slug", "asc"]] } - ); - return identityPrivileges; - }; - - return { - getPrivilegeDetailsById, - getPrivilegeDetailsBySlug, - listIdentityProjectPrivileges, - create, - updateById, - deleteById - }; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts deleted file mode 100644 index aab6b85101..0000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum IdentityProjectAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateIdentityPrivilegeDTO = { - permissions: TProjectPermissionV2Schema[]; - identityId: string; - projectId: string; - slug: string; -} & ( - | { - isTemporary: false; - } - | { - isTemporary: true; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateIdentityPrivilegeByIdDTO = { id: string } & Omit & { - data: Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - }; - -export type TDeleteIdentityPrivilegeByIdDTO = Omit & { - id: string; -}; - -export type TGetIdentityPrivilegeDetailsByIdDTO = Omit & { - id: string; -}; - -export type TListIdentityPrivilegesDTO = Omit & { - identityId: string; - projectId: string; -}; - -export type TGetIdentityPrivilegeDetailsBySlugDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts deleted file mode 100644 index 26252f2d18..0000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeDALFactory ->; - -export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts deleted file mode 100644 index ddba76920a..0000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; - -import { ActionProjectType } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; - -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { - ProjectPermissionIdentityActions, - ProjectPermissionSet, - ProjectPermissionSub -} from "../permission/project-permission"; -import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; -import { - IdentityProjectAdditionalPrivilegeTemporaryMode, - TCreateIdentityPrivilegeDTO, - TDeleteIdentityPrivilegeDTO, - TGetIdentityPrivilegeDetailsDTO, - TListIdentityPrivilegesDTO, - TUpdateIdentityPrivilegeDTO -} from "./identity-project-additional-privilege-types"; - -type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { - identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; - identityProjectDAL: Pick; - projectDAL: Pick; - permissionService: Pick; -}; - -export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeServiceFactory ->; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -export const identityProjectAdditionalPrivilegeServiceFactory = ({ - identityProjectAdditionalPrivilegeDAL, - identityProjectDAL, - permissionService, - projectDAL -}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => { - const create = async ({ - slug, - actor, - actorId, - identityId, - projectSlug, - permissions: customPermission, - actorOrgId, - actorAuthMethod, - ...dto - }: TCreateIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); - - validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateBySlug = async ({ - projectSlug, - slug, - identityId, - data, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TUpdateIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - if (data?.slug) { - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug: data.slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug && existingSlug.id !== identityPrivilege.id) - throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); - } - - const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; - validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; - if (isTemporary) { - const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; - const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: data.isTemporary, - temporaryRange: data.temporaryRange, - temporaryMode: data.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteBySlug = async ({ - actorId, - slug, - identityId, - projectSlug, - actor, - actorOrgId, - actorAuthMethod - }: TDeleteIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: identityRolePermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - identityRolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to edit more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - - const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsBySlug = async ({ - projectSlug, - identityId, - slug, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const listIdentityProjectPrivileges = async ({ - identityId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - projectSlug - }: TListIdentityPrivilegesDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ - projectMembershipId: identityProjectMembership.id - }); - return identityPrivileges.map((el) => ({ - ...el, - permissions: unpackPermissions(el.permissions) - })); - }; - - return { - create, - updateBySlug, - deleteBySlug, - getPrivilegeDetailsBySlug, - listIdentityProjectPrivileges - }; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts deleted file mode 100644 index 6a0ecee5fd..0000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum IdentityProjectAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateIdentityPrivilegeDTO = { - permissions: TProjectPermissionV2Schema[]; - identityId: string; - projectSlug: string; - slug: string; -} & ( - | { - isTemporary: false; - } - | { - isTemporary: true; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit< - TProjectPermission, - "projectId" -> & { - data: Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - }; - -export type TDeleteIdentityPrivilegeDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; - -export type TGetIdentityPrivilegeDetailsDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; - -export type TListIdentityPrivilegesDTO = Omit & { - identityId: string; - projectSlug: string; -}; diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts index 2808108dfd..27f59a99f9 100644 --- a/backend/src/ee/services/kmip/kmip-operation-service.ts +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -183,7 +183,8 @@ export const kmipOperationServiceFactory = ({ algorithm: completeKeyDetails.internalKms.encryptionAlgorithm, isActive: !key.isDisabled, createdAt: key.createdAt, - updatedAt: key.updatedAt + updatedAt: key.updatedAt, + kmipMetadata: key.kmipMetadata as Record }; }; @@ -373,7 +374,8 @@ export const kmipOperationServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId + actorOrgId, + kmipMetadata }: TKmipRegisterDTO) => { const { permission } = await permissionService.getOrgPermission( actor, @@ -405,7 +407,8 @@ export const kmipOperationServiceFactory = ({ isReserved: false, projectId, keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, - orgId: project.orgId + orgId: project.orgId, + kmipMetadata }); return kmsKey; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts index 81d0d8766b..c37d511c66 100644 --- a/backend/src/ee/services/kmip/kmip-types.ts +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -78,6 +78,7 @@ export type TKmipRegisterDTO = { name: string; key: string; algorithm: SymmetricKeyAlgorithm; + kmipMetadata?: Record; } & KmipOperationBaseDTO; export type TSetupOrgKmipDTO = { 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 8643ecdac4..43ca5ab3d1 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -12,12 +12,12 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; 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"; @@ -49,13 +49,13 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal"; type TLdapConfigServiceFactoryDep = { ldapConfigDAL: Pick; ldapGroupMapDAL: Pick; - orgMembershipDAL: Pick; orgDAL: Pick< TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; groupDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -87,9 +87,9 @@ export const ldapConfigServiceFactory = ({ ldapConfigDAL, ldapGroupMapDAL, orgDAL, - orgMembershipDAL, groupDAL, - groupProjectDAL, + membershipGroupDAL, + membershipRoleDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -388,25 +388,33 @@ export const ldapConfigServiceFactory = ({ await userDAL.transaction(async (tx) => { const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgDAL.createMembership( + const membership = await orgDAL.createMembership( { - userId: userAlias.userId, - orgId, - role, - roleId, + actorUserId: userAlias.userId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); } else if (orgMembership.status === OrgMembershipStatus.Invited) { await orgDAL.updateMembershipById( orgMembership.id, @@ -459,8 +467,9 @@ export const ldapConfigServiceFactory = ({ const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: newUserAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -469,16 +478,22 @@ export const ldapConfigServiceFactory = ({ await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.LDAP); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: newUser.id, - inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + actorUserId: newUser.id, + scopeOrgId: orgId, + scope: AccessScope.Organization, 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 - isActive: true + isActive: true, + inviteEmail: email.toLowerCase() + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId }, tx ); @@ -542,10 +557,10 @@ export const ldapConfigServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); } @@ -566,7 +581,7 @@ export const ldapConfigServiceFactory = ({ userIds: [newUser.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx }); diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index cfea2573d1..891d609221 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; export type TLicenseDALFactory = ReturnType; @@ -9,14 +9,14 @@ export type TLicenseDALFactory = ReturnType; export const licenseDALFactory = (db: TDbClient) => { const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => { try { - const doc = await (tx || db.replicaNode())(TableName.OrgMembership) - .where({ status: OrgMembershipStatus.Accepted }) + const doc = await (tx || db.replicaNode())(TableName.Membership) + .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .andWhere((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); } }) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) .count(); return Number(doc?.[0]?.count ?? 0); @@ -28,24 +28,27 @@ export const licenseDALFactory = (db: TDbClient) => { const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { try { // count org users - const userDoc = await (tx || db.replicaNode())(TableName.OrgMembership) - .where({ status: OrgMembershipStatus.Accepted }) + const userDoc = await (tx || db.replicaNode())(TableName.Membership) + .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) + .whereNotNull(`${TableName.Membership}.actorUserId`) .andWhere((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); } }) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) .count(); const userCount = Number(userDoc?.[0].count); // count org identities - const identityDoc = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) + const identityDoc = await (tx || db.replicaNode())(TableName.Membership) + .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); } }) .count(); diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 544eeae379..c2e67908ff 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -488,7 +488,7 @@ export const licenseServiceFactory = ({ const getUsageMetrics = async (orgId: string) => { const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ orgDAL.countAllOrgMembers(orgId), - identityOrgMembershipDAL.countAllOrgIdentities({ orgId }), + identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }), projectDAL.countOfOrgProjects(orgId) ]); diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 445ace2b5e..c2672a94e2 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; @@ -19,12 +19,12 @@ import { OrgServiceActor } from "@app/lib/types"; import { ActorType, 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; 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"; @@ -62,11 +62,12 @@ type TOidcConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - orgMembershipDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; - permissionService: Pick; + permissionService: Pick; oidcConfigDAL: Pick; groupDAL: Pick; userGroupMembershipDAL: Pick< @@ -78,7 +79,6 @@ type TOidcConfigServiceFactoryDep = { | "delete" | "filterProjectsByUserMembership" >; - groupProjectDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -90,7 +90,6 @@ export type TOidcConfigServiceFactory = ReturnType { - await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId); + await permissionService.getOrgPermission(ActorType.USER, actor.id, orgId, actor.authMethod, actor.orgId); const oidcConfig = await oidcConfigDAL.findOne({ orgId, diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index a35765a2ce..74a2c74ae9 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -1,4 +1,5 @@ import knex, { Knex } from "knex"; +import tls, { PeerCertificate } from "tls"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; @@ -30,7 +31,12 @@ const getConnectionConfig = ( ? { rejectUnauthorized: sslRejectUnauthorized, ca: sslCertificate, - servername: host + servername: host, + // When using proxy, we need to bypass hostname validation since we connect to localhost + // but validate the certificate against the actual hostname + checkServerIdentity: (hostname: string, cert: PeerCertificate) => { + return tls.checkServerIdentity(host, cert); + } } : false }; @@ -114,6 +120,10 @@ export const sqlResourceFactory: TPamResourceFactory Promise< - { - status: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - role: string; - isActive: boolean; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - permissions?: unknown; - userId?: string | null | undefined; - roleId?: string | null | undefined; - inviteEmail?: string | null | undefined; - projectFavorites?: string[] | null | undefined; - customRoleSlug?: string | null | undefined; - orgAuthEnforced?: boolean | null | undefined; - orgGoogleSsoAuthEnforced: boolean; - } & { - groups: { - id: string; - updatedAt: Date; - createdAt: Date; - role: string; - roleId: string | null | undefined; - customRolePermission: unknown; - name: string; - slug: string; - orgId: string; - }[]; - } - >; - getOrgIdentityPermission: ( - identityId: string, - orgId: string - ) => Promise< - | (TIdentityOrgMemberships & { - orgAuthEnforced: boolean | null | undefined; - shouldUseNewPrivilegeSystem: boolean; - permissions?: unknown; - }) - | undefined - >; - getProjectPermission: ( - userId: string, - projectId: string - ) => Promise< - | { - roles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - additionalPrivileges: { - id: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - orgId: string; - orgAuthEnforced: boolean | null | undefined; - orgGoogleSsoAuthEnforced: boolean; - orgRole: OrgMembershipRole; - userId: string; - projectId: string; - username: string; - projectType?: string | null; - id: string; - createdAt: Date; - updatedAt: Date; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - metadata: { - id: string; - key: string; - value: string; - }[]; - userGroupRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - projecMembershiptRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - } - | undefined - >; - getProjectIdentityPermission: ( - identityId: string, - projectId: string - ) => Promise< - | { - roles: { - id: string; - createdAt: Date; - updatedAt: Date; - isTemporary: boolean; - role: string; - projectMembershipId: string; - temporaryRange?: string | null | undefined; - permissions?: unknown; - customRoleId?: string | null | undefined; - temporaryMode?: string | null | undefined; - temporaryAccessStartTime?: Date | null | undefined; - temporaryAccessEndTime?: Date | null | undefined; - customRoleSlug?: string | null | undefined; - }[]; - additionalPrivileges: { - id: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - isTemporary: boolean; - }[]; - id: string; - identityId: string; - username: string; - projectId: string; - createdAt: Date; - updatedAt: Date; - orgId: string; - projectType?: string | null; - shouldUseNewPrivilegeSystem: boolean; - orgAuthEnforced: boolean; - metadata: { - id: string; - key: string; - value: string; - }[]; - } - | undefined - >; - getProjectUserPermissions: (projectId: string) => Promise< { roles: { id: string; @@ -198,45 +76,19 @@ export interface TPermissionDALFactory { temporaryAccessEndTime: Date | null | undefined; isTemporary: boolean; }[]; - orgId: string; - orgAuthEnforced: boolean | null | undefined; userId: string; - projectId: string; username: string; - projectType?: string | null; - id: string; - createdAt: Date; - updatedAt: Date; metadata: { id: string; key: string; value: string; }[]; - userGroupRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - projectMembershipRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; }[] >; - getProjectIdentityPermissions: (projectId: string) => Promise< + getProjectIdentityPermissions: ( + projectId: string, + orgId: string + ) => Promise< { roles: { id: string; @@ -244,7 +96,6 @@ export interface TPermissionDALFactory { updatedAt: Date; isTemporary: boolean; role: string; - projectMembershipId: string; temporaryRange?: string | null | undefined; permissions?: unknown; customRoleId?: string | null | undefined; @@ -269,8 +120,6 @@ export interface TPermissionDALFactory { createdAt: Date; updatedAt: Date; orgId: string; - projectType?: string | null; - orgAuthEnforced: boolean; metadata: { id: string; key: string; @@ -310,128 +159,203 @@ export interface TPermissionDALFactory { }[]; }[] >; + getPermission: (dto: { + scopeData: AccessScopeData; + actorId: string; + actorType: ActorType.IDENTITY | ActorType.USER; + tx?: Knex; + }) => Promise; } export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { - const getOrgPermission: TPermissionDALFactory["getOrgPermission"] = async (userId: string, orgId: string) => { + const getPermission: TPermissionDALFactory["getPermission"] = async ({ scopeData, tx, actorId, actorType }) => { try { - const groupSubQuery = db(TableName.Groups) - .where(`${TableName.Groups}.orgId`, orgId) - .join(TableName.UserGroupMembership, (queryBuilder) => { - queryBuilder - .on(`${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) - .andOn(`${TableName.UserGroupMembership}.userId`, db.raw("?", [userId])); - }) - .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) - .select( - db.ref("id").withSchema(TableName.Groups).as("groupId"), - db.ref("orgId").withSchema(TableName.Groups).as("groupOrgId"), - db.ref("name").withSchema(TableName.Groups).as("groupName"), - db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), - db.ref("role").withSchema(TableName.Groups).as("groupRole"), - db.ref("roleId").withSchema(TableName.Groups).as("groupRoleId"), - db.ref("createdAt").withSchema(TableName.Groups).as("groupCreatedAt"), - db.ref("updatedAt").withSchema(TableName.Groups).as("groupUpdatedAt"), - db.ref("permissions").withSchema(TableName.OrgRoles).as("groupCustomRolePermission") - ); + // akhilmhdh: when group has another group like sub group we would need recursively go down + const userGroupSubquery = (tx || db)(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, scopeData.orgId) + .where(`${TableName.UserGroupMembership}.userId`, actorId) + .select(db.ref("id").withSchema(TableName.Groups)); - const membership = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .where(`${TableName.OrgMembership}.userId`, userId) - .leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`) - .leftJoin[0]>( - groupSubQuery.as("userGroups"), - "userGroups.groupOrgId", - db.raw("?", [orgId]) - ) - .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + const docs = await (tx || db) + .replicaNode()(TableName.Membership) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.AdditionalPrivilege, (qb) => { + if (actorType === ActorType.IDENTITY) { + qb.on(`${TableName.Membership}.actorIdentityId`, `${TableName.AdditionalPrivilege}.actorIdentityId`); + } else { + qb.on(`${TableName.Membership}.actorUserId`, `${TableName.AdditionalPrivilege}.actorUserId`); + } + + if (scopeData.scope === AccessScope.Organization) { + qb.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.AdditionalPrivilege}.orgId`); + } else if (scopeData.scope === AccessScope.Project) { + qb.andOn(`${TableName.Membership}.scopeProjectId`, `${TableName.AdditionalPrivilege}.projectId`); + } else { + qb.andOn(`${TableName.Membership}.scopeNamespaceId`, `${TableName.AdditionalPrivilege}.namespaceId`); + } + }) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + if (actorType === ActorType.USER) { + void queryBuilder + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + } else if (actorType === ActorType.IDENTITY) { + void queryBuilder + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + } + }) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where((qb) => { + if (actorType === ActorType.USER) { + void qb + .where(`${TableName.Membership}.actorUserId`, actorId) + .orWhereIn(`${TableName.Membership}.actorGroupId`, userGroupSubquery); + } else if (actorType === ActorType.IDENTITY) { + void qb.where(`${TableName.Membership}.actorIdentityId`, actorId); + } + }) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .select(selectAllTableCols(TableName.Membership)) .select( - selectAllTableCols(TableName.OrgMembership), + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("permissions").withSchema(TableName.Role).as("customRolePermission"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("name").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeName"), + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegePermissions"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeCreatedAt"), + db.ref("updatedAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeUpdatedAt"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), - db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), - db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), - db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), - db.ref("groupId").withSchema("userGroups"), - db.ref("groupOrgId").withSchema("userGroups"), - db.ref("groupName").withSchema("userGroups"), - db.ref("groupSlug").withSchema("userGroups"), - db.ref("groupRole").withSchema("userGroups"), - db.ref("groupRoleId").withSchema("userGroups"), - db.ref("groupCreatedAt").withSchema("userGroups"), - db.ref("groupUpdatedAt").withSchema("userGroups"), - db.ref("groupCustomRolePermission").withSchema("userGroups") + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled") ); - const [formatedDoc] = sqlNestRelationships({ - data: membership, + const data = sqlNestRelationships({ + data: docs, key: "id", parentMapper: (el) => - OrgMembershipsSchema.extend({ - permissions: z.unknown(), + MembershipsSchema.extend({ orgAuthEnforced: z.boolean().optional().nullable(), + shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), orgGoogleSsoAuthEnforced: z.boolean(), - bypassOrgAuthEnabled: z.boolean(), - customRoleSlug: z.string().optional().nullable(), - shouldUseNewPrivilegeSystem: z.boolean() + bypassOrgAuthEnabled: z.boolean() }).parse(el), childrenMapper: [ { - key: "groupId", - label: "groups" as const, + key: "additionalPrivilegeId", + label: "additionalPrivileges" as const, mapper: ({ - groupId, - groupUpdatedAt, - groupCreatedAt, - groupRole, - groupRoleId, - groupCustomRolePermission, - groupName, - groupSlug, - groupOrgId + additionalPrivilegeId, + additionalPrivilegePermissions, + additionalPrivilegeIsTemporary, + additionalPrivilegeTemporaryMode, + additionalPrivilegeTemporaryRange, + additionalPrivilegeTemporaryAccessEndTime, + additionalPrivilegeTemporaryAccessStartTime, + additionalPrivilegeCreatedAt, + additionalPrivilegeUpdatedAt }) => ({ - id: groupId, - updatedAt: groupUpdatedAt, - createdAt: groupCreatedAt, - role: groupRole, - roleId: groupRoleId, - customRolePermission: groupCustomRolePermission, - name: groupName, - slug: groupSlug, - orgId: groupOrgId + id: additionalPrivilegeId, + permissions: additionalPrivilegePermissions, + temporaryRange: additionalPrivilegeTemporaryRange, + temporaryMode: additionalPrivilegeTemporaryMode, + temporaryAccessStartTime: additionalPrivilegeTemporaryAccessStartTime, + temporaryAccessEndTime: additionalPrivilegeTemporaryAccessEndTime, + isTemporary: additionalPrivilegeIsTemporary, + createdAt: additionalPrivilegeCreatedAt, + updatedAt: additionalPrivilegeUpdatedAt + }) + }, + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + customRolePermission, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + permissions: customRolePermission, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue }) } ] }); - return formatedDoc; + return data; } catch (error) { - throw new DatabaseError({ error, name: "GetOrgPermission" }); - } - }; - - const getOrgIdentityPermission: TPermissionDALFactory["getOrgIdentityPermission"] = async ( - identityId: string, - orgId: string - ) => { - try { - 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) - .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) - .select(selectAllTableCols(TableName.IdentityOrgMembership)) - .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) - .select("permissions") - .select(db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization)) - .first(); - - return membership; - } catch (error) { - throw new DatabaseError({ error, name: "GetOrgIdentityPermission" }); + throw new DatabaseError({ error, name: "Get Permission" }); } }; @@ -441,55 +365,41 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { ) => { try { const docs = await db - .replicaNode()(TableName.GroupProjectMembership) - .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`) - .join( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - { groupCustomRoles: TableName.ProjectRoles }, - `${TableName.GroupProjectMembershipRole}.customRoleId`, + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin( + { groupCustomRoles: TableName.Role }, + `${TableName.MembershipRole}.customRoleId`, `groupCustomRoles.id` ) - .where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId) + .where(`${TableName.Membership}.scopeProjectId`, "=", projectId) .where((bd) => { if (filterGroupId) { - void bd.where(`${TableName.GroupProjectMembership}.groupId`, "=", filterGroupId); + void bd.where(`${TableName.Membership}.actorGroupId`, "=", filterGroupId); } }) .select( - db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Membership).as("membershipId"), db.ref("id").withSchema(TableName.Groups).as("groupId"), db.ref("name").withSchema(TableName.Groups).as("groupName"), db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"), db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"), - db - .ref("customRoleId") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleCustomRoleId"), - db - .ref("isTemporary") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleIsTemporary"), - db - .ref("temporaryMode") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleTemporaryMode"), - db - .ref("temporaryRange") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleTemporaryRange"), + db.ref("id").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("groupProjectMembershipRole"), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleCustomRoleId"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.GroupProjectMembershipRole) + .withSchema(TableName.MembershipRole) .as("groupProjectMembershipRoleTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.GroupProjectMembershipRole) + .withSchema(TableName.MembershipRole) .as("groupProjectMembershipRoleTemporaryAccessEndTime") ); @@ -551,246 +461,148 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { } }; - const getProjectUserPermissions: TPermissionDALFactory["getProjectUserPermissions"] = async (projectId: string) => { + const getProjectUserPermissions: TPermissionDALFactory["getProjectUserPermissions"] = async ( + projectId: string, + orgId: string + ) => { + const userGroupSubquery = db(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, orgId) + .select(db.ref("id").withSchema(TableName.Groups)); + try { const docs = await db .replicaNode()(TableName.Users) .where("isGhost", "=", false) - .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { - void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])); + .join(TableName.Membership, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.AdditionalPrivilege, (qb) => { + qb.on(`${TableName.Membership}.actorUserId`, `${TableName.AdditionalPrivilege}.actorUserId`).andOn( + `${TableName.Membership}.scopeOrgId`, + `${TableName.AdditionalPrivilege}.orgId` + ); }) - .leftJoin( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - { groupCustomRoles: TableName.ProjectRoles }, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `groupCustomRoles.id` - ) - .join(TableName.ProjectMembership, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); - }) - .leftJoin( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); - }) - .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) - .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where((qb) => { + void qb + .whereNotNull(`${TableName.Membership}.actorUserId`) + .orWhereIn(`${TableName.Membership}.actorGroupId`, userGroupSubquery); + }) + .where((qb) => { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, projectId); }) .select( db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("username").withSchema(TableName.Users).as("username"), - // groups specific - db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"), - db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"), - db - .ref("customRoleId") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleCustomRoleId"), - db - .ref("isTemporary") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleIsTemporary"), - db - .ref("temporaryMode") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryMode"), - db - .ref("temporaryRange") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryRange"), + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("permissions").withSchema(TableName.Role).as("customRolePermission"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessStartTime"), + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessEndTime"), - // user specific - db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), - db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryRange"), + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("name").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeName"), + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegePermissions"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessStartTime"), + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessEndTime"), - db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"), - db - .ref("permissions") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryRange"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessEndTime"), + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeCreatedAt"), + db.ref("updatedAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeUpdatedAt"), // general db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), - db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), - db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), - db.ref("orgId").withSchema(TableName.Project), - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("id").withSchema(TableName.Project).as("projectId") + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") ); const userPermissions = sqlNestRelationships({ data: docs, key: "userId", - parentMapper: ({ - orgId, - username, - orgAuthEnforced, - membershipId, - groupMembershipId, - membershipCreatedAt, - groupMembershipCreatedAt, - groupMembershipUpdatedAt, - membershipUpdatedAt, - projectType, - userId - }) => ({ - orgId, - orgAuthEnforced, + parentMapper: ({ username, userId }) => ({ userId, projectId, - username, - projectType, - id: membershipId || groupMembershipId, - createdAt: membershipCreatedAt || groupMembershipCreatedAt, - updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt + username }), childrenMapper: [ { - key: "userGroupProjectMembershipRoleId", - label: "userGroupRoles" as const, - mapper: ({ - userGroupProjectMembershipRoleId, - userGroupProjectMembershipRole, - userGroupProjectMembershipRolePermission, - userGroupProjectMembershipRoleCustomRoleSlug, - userGroupProjectMembershipRoleIsTemporary, - userGroupProjectMembershipRoleTemporaryMode, - userGroupProjectMembershipRoleTemporaryAccessEndTime, - userGroupProjectMembershipRoleTemporaryAccessStartTime, - userGroupProjectMembershipRoleTemporaryRange - }) => ({ - id: userGroupProjectMembershipRoleId, - role: userGroupProjectMembershipRole, - customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug, - permissions: userGroupProjectMembershipRolePermission, - temporaryRange: userGroupProjectMembershipRoleTemporaryRange, - temporaryMode: userGroupProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userGroupProjectMembershipRoleIsTemporary - }) - }, - { - key: "userProjectMembershipRoleId", - label: "projectMembershipRoles" as const, - mapper: ({ - userProjectMembershipRoleId, - userProjectMembershipRole, - userProjectCustomRolePermission, - userProjectMembershipRoleIsTemporary, - userProjectMembershipRoleTemporaryMode, - userProjectMembershipRoleTemporaryRange, - userProjectMembershipRoleTemporaryAccessEndTime, - userProjectMembershipRoleTemporaryAccessStartTime, - userProjectMembershipRoleCustomRoleSlug - }) => ({ - id: userProjectMembershipRoleId, - role: userProjectMembershipRole, - customRoleSlug: userProjectMembershipRoleCustomRoleSlug, - permissions: userProjectCustomRolePermission, - temporaryRange: userProjectMembershipRoleTemporaryRange, - temporaryMode: userProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userProjectMembershipRoleIsTemporary - }) - }, - { - key: "userAdditionalPrivilegesId", + key: "additionalPrivilegeId", label: "additionalPrivileges" as const, mapper: ({ - userAdditionalPrivilegesId, - userAdditionalPrivilegesPermissions, - userAdditionalPrivilegesIsTemporary, - userAdditionalPrivilegesTemporaryMode, - userAdditionalPrivilegesTemporaryRange, - userAdditionalPrivilegesTemporaryAccessEndTime, - userAdditionalPrivilegesTemporaryAccessStartTime + additionalPrivilegeId, + additionalPrivilegePermissions, + additionalPrivilegeIsTemporary, + additionalPrivilegeTemporaryMode, + additionalPrivilegeTemporaryRange, + additionalPrivilegeTemporaryAccessEndTime, + additionalPrivilegeTemporaryAccessStartTime, + additionalPrivilegeCreatedAt, + additionalPrivilegeUpdatedAt }) => ({ - id: userAdditionalPrivilegesId, - permissions: userAdditionalPrivilegesPermissions, - temporaryRange: userAdditionalPrivilegesTemporaryRange, - temporaryMode: userAdditionalPrivilegesTemporaryMode, - temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, - temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, - isTemporary: userAdditionalPrivilegesIsTemporary + id: additionalPrivilegeId, + permissions: additionalPrivilegePermissions, + temporaryRange: additionalPrivilegeTemporaryRange, + temporaryMode: additionalPrivilegeTemporaryMode, + temporaryAccessStartTime: additionalPrivilegeTemporaryAccessStartTime, + temporaryAccessEndTime: additionalPrivilegeTemporaryAccessEndTime, + isTemporary: additionalPrivilegeIsTemporary, + createdAt: additionalPrivilegeCreatedAt, + updatedAt: additionalPrivilegeUpdatedAt + }) + }, + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + customRolePermission, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + permissions: customRolePermission, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt }) }, { @@ -808,17 +620,11 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { return userPermissions .map((userPermission) => { if (!userPermission) return undefined; - if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined; + if (!userPermission?.roles?.[0]) return undefined; // when introducting cron mode change it here const activeRoles = - userPermission?.projectMembershipRoles?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - const activeGroupRoles = - userPermission?.userGroupRoles?.filter( + userPermission?.roles?.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ) ?? []; @@ -831,7 +637,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { return { ...userPermission, - roles: [...activeRoles, ...activeGroupRoles], + roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; }) @@ -841,378 +647,52 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { } }; - const getProjectPermission: TPermissionDALFactory["getProjectPermission"] = async ( - userId: string, - projectId: string - ) => { - try { - const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); - const docs = await db - .replicaNode()(TableName.Users) - .where(`${TableName.Users}.id`, userId) - .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { - void queryBuilder - .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) - // @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it - .andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups); - }) - .leftJoin( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - { groupCustomRoles: TableName.ProjectRoles }, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `groupCustomRoles.id` - ) - .leftJoin(TableName.ProjectMembership, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); - }) - .leftJoin( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); - }) - .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) - .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) - .join(TableName.OrgMembership, (qb) => { - void qb - .on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`); - }) - .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { - void queryBuilder - .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`); - }) - .select( - db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("username").withSchema(TableName.Users).as("username"), - // groups specific - db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"), - db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"), - db - .ref("customRoleId") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleCustomRoleId"), - db - .ref("isTemporary") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleIsTemporary"), - db - .ref("temporaryMode") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryMode"), - db - .ref("temporaryRange") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessEndTime"), - // user specific - db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), - db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessEndTime"), - db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"), - db - .ref("permissions") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryRange"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessEndTime"), - // general - db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), - db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), - db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), - db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), - db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), - db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), - db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"), - db.ref("orgId").withSchema(TableName.Project), - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("id").withSchema(TableName.Project).as("projectId"), - db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization) - ); - - const [userPermission] = sqlNestRelationships({ - data: docs, - key: "projectId", - parentMapper: ({ - orgId, - username, - orgAuthEnforced, - orgGoogleSsoAuthEnforced, - orgRole, - membershipId, - groupMembershipId, - membershipCreatedAt, - groupMembershipCreatedAt, - groupMembershipUpdatedAt, - membershipUpdatedAt, - projectType, - shouldUseNewPrivilegeSystem, - bypassOrgAuthEnabled - }) => ({ - orgId, - orgAuthEnforced, - orgGoogleSsoAuthEnforced, - orgRole: orgRole as OrgMembershipRole, - userId, - projectId, - username, - projectType, - id: membershipId || groupMembershipId, - createdAt: membershipCreatedAt || groupMembershipCreatedAt, - updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt, - shouldUseNewPrivilegeSystem, - bypassOrgAuthEnabled - }), - childrenMapper: [ - { - key: "userGroupProjectMembershipRoleId", - label: "userGroupRoles" as const, - mapper: ({ - userGroupProjectMembershipRoleId, - userGroupProjectMembershipRole, - userGroupProjectMembershipRolePermission, - userGroupProjectMembershipRoleCustomRoleSlug, - userGroupProjectMembershipRoleIsTemporary, - userGroupProjectMembershipRoleTemporaryMode, - userGroupProjectMembershipRoleTemporaryAccessEndTime, - userGroupProjectMembershipRoleTemporaryAccessStartTime, - userGroupProjectMembershipRoleTemporaryRange - }) => ({ - id: userGroupProjectMembershipRoleId, - role: userGroupProjectMembershipRole, - customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug, - permissions: userGroupProjectMembershipRolePermission, - temporaryRange: userGroupProjectMembershipRoleTemporaryRange, - temporaryMode: userGroupProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userGroupProjectMembershipRoleIsTemporary - }) - }, - { - key: "userProjectMembershipRoleId", - label: "projecMembershiptRoles" as const, - mapper: ({ - userProjectMembershipRoleId, - userProjectMembershipRole, - userProjectCustomRolePermission, - userProjectMembershipRoleIsTemporary, - userProjectMembershipRoleTemporaryMode, - userProjectMembershipRoleTemporaryRange, - userProjectMembershipRoleTemporaryAccessEndTime, - userProjectMembershipRoleTemporaryAccessStartTime, - userProjectMembershipRoleCustomRoleSlug - }) => ({ - id: userProjectMembershipRoleId, - role: userProjectMembershipRole, - customRoleSlug: userProjectMembershipRoleCustomRoleSlug, - permissions: userProjectCustomRolePermission, - temporaryRange: userProjectMembershipRoleTemporaryRange, - temporaryMode: userProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userProjectMembershipRoleIsTemporary - }) - }, - { - key: "userAdditionalPrivilegesId", - label: "additionalPrivileges" as const, - mapper: ({ - userAdditionalPrivilegesId, - userAdditionalPrivilegesPermissions, - userAdditionalPrivilegesIsTemporary, - userAdditionalPrivilegesTemporaryMode, - userAdditionalPrivilegesTemporaryRange, - userAdditionalPrivilegesTemporaryAccessEndTime, - userAdditionalPrivilegesTemporaryAccessStartTime - }) => ({ - id: userAdditionalPrivilegesId, - permissions: userAdditionalPrivilegesPermissions, - temporaryRange: userAdditionalPrivilegesTemporaryRange, - temporaryMode: userAdditionalPrivilegesTemporaryMode, - temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, - temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, - isTemporary: userAdditionalPrivilegesIsTemporary - }) - }, - { - key: "metadataId", - label: "metadata" as const, - mapper: ({ metadataKey, metadataValue, metadataId }) => ({ - id: metadataId, - key: metadataKey, - value: metadataValue - }) - } - ] - }); - - if (!userPermission) return undefined; - if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined; - - // when introducting cron mode change it here - const activeRoles = - userPermission?.projecMembershiptRoles?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - const activeGroupRoles = - userPermission?.userGroupRoles?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - const activeAdditionalPrivileges = - userPermission?.additionalPrivileges?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - return { - ...userPermission, - roles: [...activeRoles, ...activeGroupRoles], - additionalPrivileges: activeAdditionalPrivileges - }; - } catch (error) { - throw new DatabaseError({ error, name: "GetProjectPermission" }); - } - }; - const getProjectIdentityPermissions: TPermissionDALFactory["getProjectIdentityPermissions"] = async ( - projectId: string + projectId: string, + orgId: string ) => { try { const docs = await db - .replicaNode()(TableName.IdentityProjectMembership) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join( - // Join the Project table to later select orgId - TableName.Project, - `${TableName.IdentityProjectMembership}.projectId`, - `${TableName.Project}.id` - ) + .replicaNode()(TableName.Membership) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.AdditionalPrivilege, (qb) => { + qb.on(`${TableName.Membership}.actorIdentityId`, `${TableName.AdditionalPrivilege}.actorIdentityId`).andOn( + `${TableName.Membership}.scopeOrgId`, + `${TableName.AdditionalPrivilege}.orgId` + ); + }) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .select(selectAllTableCols(TableName.MembershipRole)) .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Membership).as("membershipId"), db.ref("id").withSchema(TableName.Identity).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles), - db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), - db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryMode"), - db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryRange"), + db.ref("createdAt").withSchema(TableName.Membership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.Membership).as("membershipUpdatedAt"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("permissions").withSchema(TableName.Role), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("identityApId"), + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("identityApPermissions"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("identityApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("identityApIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("identityApTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("identityApTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("identityApTemporaryAccessEndTime"), db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), @@ -1222,15 +702,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { const permissions = sqlNestRelationships({ data: docs, key: "identityId", - parentMapper: ({ - membershipId, - membershipCreatedAt, - membershipUpdatedAt, - orgId, - identityName, - projectType, - identityId - }) => ({ + parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, identityName, identityId }) => ({ id: membershipId, identityId, username: identityName, @@ -1238,7 +710,6 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { createdAt: membershipCreatedAt, updatedAt: membershipUpdatedAt, orgId, - projectType, // just a prefilled value orgAuthEnforced: false }), @@ -1247,7 +718,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { key: "id", label: "roles" as const, mapper: (data) => - IdentityProjectMembershipRoleSchema.extend({ + MembershipRolesSchema.extend({ permissions: z.unknown(), customRoleSlug: z.string().optional().nullable() }).parse(data) @@ -1309,170 +780,10 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { } }; - const getProjectIdentityPermission: TPermissionDALFactory["getProjectIdentityPermission"] = async ( - identityId, - projectId - ) => { - try { - const docs = await db - .replicaNode()(TableName.IdentityProjectMembership) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join( - // Join the Project table to later select orgId - TableName.Project, - `${TableName.IdentityProjectMembership}.projectId`, - `${TableName.Project}.id` - ) - .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) - .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { - void queryBuilder - .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); - }) - .where(`${TableName.IdentityProjectMembership}.identityId`, identityId) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) - .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), - db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles), - db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), - db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), - db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryMode"), - db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryAccessEndTime"), - db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), - db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), - db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") - ); - - const permission = sqlNestRelationships({ - data: docs, - key: "membershipId", - parentMapper: ({ - membershipId, - membershipCreatedAt, - membershipUpdatedAt, - orgId, - identityName, - projectType, - shouldUseNewPrivilegeSystem - }) => ({ - id: membershipId, - identityId, - username: identityName, - projectId, - createdAt: membershipCreatedAt, - updatedAt: membershipUpdatedAt, - orgId, - projectType, - shouldUseNewPrivilegeSystem, - // just a prefilled value - orgAuthEnforced: false - }), - childrenMapper: [ - { - key: "id", - label: "roles" as const, - mapper: (data) => - IdentityProjectMembershipRoleSchema.extend({ - permissions: z.unknown(), - customRoleSlug: z.string().optional().nullable() - }).parse(data) - }, - { - key: "identityApId", - label: "additionalPrivileges" as const, - mapper: ({ - identityApId, - identityApPermissions, - identityApIsTemporary, - identityApTemporaryMode, - identityApTemporaryRange, - identityApTemporaryAccessEndTime, - identityApTemporaryAccessStartTime - }) => ({ - id: identityApId, - permissions: identityApPermissions, - temporaryRange: identityApTemporaryRange, - temporaryMode: identityApTemporaryMode, - temporaryAccessEndTime: identityApTemporaryAccessEndTime, - temporaryAccessStartTime: identityApTemporaryAccessStartTime, - isTemporary: identityApIsTemporary - }) - }, - { - key: "metadataId", - label: "metadata" as const, - mapper: ({ metadataKey, metadataValue, metadataId }) => ({ - id: metadataId, - key: metadataKey, - value: metadataValue - }) - } - ] - }); - - if (!permission?.[0]) return undefined; - - // when introducting cron mode change it here - const activeRoles = permission?.[0]?.roles.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ); - const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ); - - return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; - } catch (error) { - throw new DatabaseError({ error, name: "GetProjectIdentityPermission" }); - } - }; - return { - getOrgPermission, - getOrgIdentityPermission, - getProjectPermission, - getProjectIdentityPermission, getProjectUserPermissions, getProjectIdentityPermissions, - getProjectGroupPermissions + getProjectGroupPermissions, + getPermission }; }; diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 7d61d74992..0da5ad3b25 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { z } from "zod"; -import { OrgMembershipRole, TOrganizations } from "@app/db/schemas"; +import { TOrganizations } from "@app/db/schemas"; import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; @@ -123,13 +123,13 @@ function validateOrgSSO( isOrgSsoEnforced: TOrganizations["authEnforced"], isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"], isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"], - orgRole: OrgMembershipRole + isAdmin: boolean ) { if (actorAuthMethod === undefined) { throw new UnauthorizedError({ name: "No auth method defined" }); } - if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) { + if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && isAdmin) { return; } diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index f25b84bd59..1f0e004706 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -1,8 +1,8 @@ -import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { MongoAbility } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; import { Knex } from "knex"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, TMemberships } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionSet } from "./org-permission"; @@ -49,232 +49,90 @@ export type TGetProjectPermissionArg = { actionProjectType: ActionProjectType; }; +export type TGetOrgPermissionArg = { + actor: ActorType; + actorId: string; + orgId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId?: string; +}; + export type TPermissionServiceFactory = { - getUserOrgPermission: ( - userId: string, - orgId: string, - authMethod: ActorAuthMethod, - userOrgId?: string - ) => Promise<{ - permission: MongoAbility; - membership: { - status: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - role: string; - isActive: boolean; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - permissions?: unknown; - userId?: string | null | undefined; - roleId?: string | null | undefined; - inviteEmail?: string | null | undefined; - projectFavorites?: string[] | null | undefined; - customRoleSlug?: string | null | undefined; - orgAuthEnforced?: boolean | null | undefined; - } & { - groups: { - id: string; - updatedAt: Date; - createdAt: Date; - role: string; - roleId: string | null | undefined; - customRolePermission: unknown; - name: string; - slug: string; - orgId: string; - }[]; - }; - }>; getOrgPermission: ( type: ActorType, id: string, orgId: string, authMethod: ActorAuthMethod, actorOrgId: string | undefined - ) => Promise< - | { - permission: MongoAbility; - membership: { - status: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - role: string; - isActive: boolean; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - permissions?: unknown; - userId?: string | null | undefined; - roleId?: string | null | undefined; - inviteEmail?: string | null | undefined; - projectFavorites?: string[] | null | undefined; - customRoleSlug?: string | null | undefined; - orgAuthEnforced?: boolean | null | undefined; - } & { - groups: { - id: string; - updatedAt: Date; - createdAt: Date; - role: string; - roleId: string | null | undefined; - customRolePermission: unknown; - name: string; - slug: string; - orgId: string; - }[]; - }; + ) => Promise<{ + permission: MongoAbility; + memberships: Array< + TMemberships & { + roles: { role: string; customRoleSlug?: string | null }[]; + shouldUseNewPrivilegeSystem?: boolean | null; } - | { - permission: MongoAbility; - membership: { - id: string; - role: string; - createdAt: Date; - updatedAt: Date; - orgId: string; - roleId?: string | null | undefined; - permissions?: unknown; - identityId: string; - orgAuthEnforced: boolean | null | undefined; - shouldUseNewPrivilegeSystem: boolean; - }; - } - >; - getUserProjectPermission: ({ - userId, - projectId, - authMethod, - userOrgId, - actionProjectType - }: TGetUserProjectPermissionArg) => Promise<{ - permission: MongoAbility; - membership: { - id: string; - createdAt: Date; - updatedAt: Date; - userId: string; - projectId: string; - } & { - orgAuthEnforced: boolean | null | undefined; - orgId: string; - roles: Array<{ - role: string; - }>; - shouldUseNewPrivilegeSystem: boolean; - }; + >; hasRole: (role: string) => boolean; }>; - getProjectPermission: ( - arg: TGetProjectPermissionArg - ) => Promise< - T extends ActorType.SERVICE - ? { - permission: MongoAbility; - membership: { - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (arg: string) => boolean; - } - : { - permission: MongoAbility; - membership: (T extends ActorType.USER - ? { - id: string; - createdAt: Date; - updatedAt: Date; - userId: string; - projectId: string; - } - : { - id: string; - createdAt: Date; - updatedAt: Date; - projectId: string; - identityId: string; - }) & { - orgAuthEnforced: boolean | null | undefined; - orgId: string; - roles: Array<{ - role: string; - }>; - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (role: string) => boolean; - } - >; - getProjectPermissions: (projectId: string) => Promise<{ + getProjectPermission: (arg: TGetProjectPermissionArg) => Promise<{ + permission: MongoAbility; + memberships: Array; + hasRole: (role: string) => boolean; + }>; + getProjectPermissions: ( + projectId: string, + orgId: string + ) => Promise<{ userPermissions: { permission: MongoAbility; id: string; name: string; - membershipId: string; }[]; identityPermissions: { permission: MongoAbility; id: string; name: string; - membershipId: string; }[]; groupPermissions: { permission: MongoAbility; id: string; name: string; - membershipId: string; }[]; }>; - getOrgPermissionByRole: ( - role: string, + getOrgPermissionByRoles: ( + roles: string[], orgId: string ) => Promise< - | { - permission: MongoAbility; - role: { - name: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - slug: string; - permissions?: unknown; - description?: string | null | undefined; - }; - } - | { - permission: MongoAbility; - role?: undefined; - } + { + permission: MongoAbility; + role?: { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + slug: string; + permissions?: unknown; + description?: string | null | undefined; + }; + }[] >; - getProjectPermissionByRole: ( - role: string, + getProjectPermissionByRoles: ( + roles: string[], projectId: string ) => Promise< - | { - permission: MongoAbility; - role: { - name: string; - version: number; - id: string; - createdAt: Date; - updatedAt: Date; - projectId: string; - slug: string; - permissions?: unknown; - description?: string | null | undefined; - }; - } - | { - permission: MongoAbility; - role?: undefined; - } + { + permission: MongoAbility; + role?: { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + slug: string; + permissions?: unknown; + description?: string | null | undefined; + }; + }[] >; - buildOrgPermission: (orgUserRoles: TBuildOrgPermissionDTO) => MongoAbility; - buildProjectPermissionRules: ( - projectUserRoles: TBuildProjectPermissionDTO - ) => RawRuleOf>[]; checkGroupProjectPermission: ({ groupId, projectId, diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 9e94e14158..2d879b4fdf 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -1,17 +1,15 @@ import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, unpackRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; -import { MongoQuery } from "@ucast/mongo2js"; import handlebars from "handlebars"; import { Knex } from "knex"; import { + AccessScope, ActionProjectType, OrgMembershipRole, ProjectMembershipRole, - ServiceTokenScopes, - TIdentityProjectMemberships, - TProjectMemberships + ServiceTokenScopes } from "@app/db/schemas"; import { cryptographicOperatorPermissions, @@ -25,12 +23,12 @@ import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/ import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { objectify } from "@app/lib/fn"; -import { logger } from "@app/lib/logger"; import { ActorType } from "@app/services/auth/auth-type"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal"; +import { TRoleDALFactory } from "@app/services/role/role-dal"; import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; @@ -38,56 +36,86 @@ import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns"; import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO, - TGetIdentityProjectPermissionArg, - TGetProjectPermissionArg, TGetServiceTokenProjectPermissionArg, - TGetUserProjectPermissionArg, TPermissionServiceFactory } from "./permission-service-types"; import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission"; +const buildOrgPermissionRules = (orgUserRoles: TBuildOrgPermissionDTO) => { + const rules = orgUserRoles + .map(({ role, permissions }) => { + switch (role) { + case OrgMembershipRole.Admin: + return orgAdminPermissions; + case OrgMembershipRole.Member: + return orgMemberPermissions; + case OrgMembershipRole.NoAccess: + return orgNoAccessPermissions; + case OrgMembershipRole.Custom: + return unpackRules>>( + permissions as PackRule>>[] + ); + default: + throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` }); + } + }) + .reduce((prev, curr) => prev.concat(curr), []); + + return rules; +}; + +const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => { + const rules = projectUserRoles + .map(({ role, permissions }) => { + switch (role) { + case ProjectMembershipRole.Admin: + return projectAdminPermissions; + case ProjectMembershipRole.Member: + return projectMemberPermissions; + case ProjectMembershipRole.Viewer: + return projectViewerPermission; + case ProjectMembershipRole.NoAccess: + return projectNoAccessPermissions; + case ProjectMembershipRole.SshHostBootstrapper: + return sshHostBootstrapPermissions; + case ProjectMembershipRole.KmsCryptographicOperator: + return cryptographicOperatorPermissions; + case ProjectMembershipRole.Custom: { + return unpackRules>>( + permissions as PackRule>>[] + ); + } + default: + throw new NotFoundError({ + name: "ProjectRoleInvalid", + message: `Project role '${role}' not found` + }); + } + }) + .reduce((prev, curr) => prev.concat(curr), []); + + return rules; +}; + type TPermissionServiceFactoryDep = { - orgRoleDAL: Pick; - projectRoleDAL: Pick; serviceTokenDAL: Pick; projectDAL: Pick; permissionDAL: TPermissionDALFactory; keyStore: TKeyStoreFactory; + userDAL: Pick; + identityDAL: Pick; + roleDAL: Pick; }; export const permissionServiceFactory = ({ permissionDAL, - orgRoleDAL, - projectRoleDAL, serviceTokenDAL, projectDAL, - keyStore + userDAL, + identityDAL, + keyStore, + roleDAL }: TPermissionServiceFactoryDep): TPermissionServiceFactory => { - const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { - const rules = orgUserRoles - .map(({ role, permissions }) => { - switch (role) { - case OrgMembershipRole.Admin: - return orgAdminPermissions; - case OrgMembershipRole.Member: - return orgMemberPermissions; - case OrgMembershipRole.NoAccess: - return orgNoAccessPermissions; - case OrgMembershipRole.Custom: - return unpackRules>>( - permissions as PackRule>>[] - ); - default: - throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` }); - } - }) - .reduce((prev, curr) => prev.concat(curr), []); - - return createMongoAbility(rules, { - conditionsMatcher - }); - }; - const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => { const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId); await keyStore.pgIncrementBy(projectPermissionDalVersionKey, { @@ -97,147 +125,59 @@ export const permissionServiceFactory = ({ }); }; - const calculateProjectPermissionTtl = (membership: unknown): number => { - const now = new Date(); - let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds; - - const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => { - return items - .filter((item) => item.isTemporary && item.temporaryAccessEndTime) - .map((item) => item.temporaryAccessEndTime!) - .filter((endTime) => endTime > now) - .reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null); - }; - - const roleTimes: Date[] = []; - const additionalPrivilegeTimes: Date[] = []; - - if ( - membership && - typeof membership === "object" && - "roles" in membership && - Array.isArray((membership as Record).roles) - ) { - const roles = (membership as Record).roles as Array<{ - temporaryAccessEndTime?: Date | null; - isTemporary?: boolean; - }>; - const minRoleEndTime = getMinEndTime(roles); - if (minRoleEndTime) roleTimes.push(minRoleEndTime); - } - - if ( - membership && - typeof membership === "object" && - "additionalPrivileges" in membership && - Array.isArray((membership as Record).additionalPrivileges) - ) { - const additionalPrivileges = (membership as Record).additionalPrivileges as Array<{ - temporaryAccessEndTime?: Date | null; - isTemporary?: boolean; - }>; - const minAdditionalEndTime = getMinEndTime(additionalPrivileges); - if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime); - } - - const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes]; - if (allEndTimes.length > 0) { - const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min)); - const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000); - minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry)); - } - - return minTtl; - }; - - const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => { - const rules = projectUserRoles - .map(({ role, permissions }) => { - switch (role) { - case ProjectMembershipRole.Admin: - return projectAdminPermissions; - case ProjectMembershipRole.Member: - return projectMemberPermissions; - case ProjectMembershipRole.Viewer: - return projectViewerPermission; - case ProjectMembershipRole.NoAccess: - return projectNoAccessPermissions; - case ProjectMembershipRole.SshHostBootstrapper: - return sshHostBootstrapPermissions; - case ProjectMembershipRole.KmsCryptographicOperator: - return cryptographicOperatorPermissions; - case ProjectMembershipRole.Custom: { - return unpackRules>>( - permissions as PackRule>>[] - ); - } - default: - throw new NotFoundError({ - name: "ProjectRoleInvalid", - message: `Project role '${role}' not found` - }); - } - }) - .reduce((prev, curr) => prev.concat(curr), []); - - return rules; - }; - - /* - * Get user permission in an organization - */ - const getUserOrgPermission: TPermissionServiceFactory["getUserOrgPermission"] = async ( - userId, - orgId, - authMethod, - userOrgId - ) => { - // when token is scoped, ensure the passed org id is same as user org id - if (userOrgId && userOrgId !== orgId) - throw new ForbiddenRequestError({ message: "Invalid user token. Scoped to different organization." }); - const membership = await permissionDAL.getOrgPermission(userId, orgId); - if (!membership) throw new ForbiddenRequestError({ name: "You are not apart of this organization" }); - if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { - throw new BadRequestError({ name: "Custom organization permission not found" }); - } - - // If the org ID is API_KEY, the request is being made with an API Key. - // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. - - // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. - // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. - if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { - throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); - } - - validateOrgSSO( - authMethod, - membership.orgAuthEnforced, - membership.orgGoogleSsoAuthEnforced, - membership.bypassOrgAuthEnabled, - membership.role as OrgMembershipRole - ); - - const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( - membership?.groups?.map(({ role, customRolePermission }) => ({ - role, - permissions: customRolePermission - })) || [] - ); - return { permission: buildOrgPermission(finalPolicyRoles), membership }; - }; - - const getIdentityOrgPermission = async (identityId: string, orgId: string) => { - const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId); - if (!membership) throw new ForbiddenRequestError({ name: "Identity is not apart of this organization" }); - if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { - throw new NotFoundError({ name: `Custom organization permission not found for identity ${identityId}` }); - } - return { - permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]), - membership - }; - }; + // akhilmdhh: will bring this up later + // const calculateProjectPermissionTtl = (membership: unknown): number => { + // const now = new Date(); + // let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds; + // + // const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => { + // return items + // .filter((item) => item.isTemporary && item.temporaryAccessEndTime) + // .map((item) => item.temporaryAccessEndTime!) + // .filter((endTime) => endTime > now) + // .reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null); + // }; + // + // const roleTimes: Date[] = []; + // const additionalPrivilegeTimes: Date[] = []; + // + // if ( + // membership && + // typeof membership === "object" && + // "roles" in membership && + // Array.isArray((membership as Record).roles) + // ) { + // const roles = (membership as Record).roles as Array<{ + // temporaryAccessEndTime?: Date | null; + // isTemporary?: boolean; + // }>; + // const minRoleEndTime = getMinEndTime(roles); + // if (minRoleEndTime) roleTimes.push(minRoleEndTime); + // } + // + // if ( + // membership && + // typeof membership === "object" && + // "additionalPrivileges" in membership && + // Array.isArray((membership as Record).additionalPrivileges) + // ) { + // const additionalPrivileges = (membership as Record).additionalPrivileges as Array<{ + // temporaryAccessEndTime?: Date | null; + // isTemporary?: boolean; + // }>; + // const minAdditionalEndTime = getMinEndTime(additionalPrivileges); + // if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime); + // } + // + // const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes]; + // if (allEndTimes.length > 0) { + // const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min)); + // const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000); + // minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry)); + // } + // + // return minTtl; + // }; const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( type, @@ -246,199 +186,62 @@ export const permissionServiceFactory = ({ authMethod, actorOrgId ) => { - switch (type) { - case ActorType.USER: - return getUserOrgPermission(id, orgId, authMethod, actorOrgId); - case ActorType.IDENTITY: - return getIdentityOrgPermission(id, orgId); - default: - throw new BadRequestError({ - message: "Invalid actor provided", - name: "Get org permission" - }); - } - }; - - // instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like - // admin member or user defined ones like biller etc - const getOrgPermissionByRole: TPermissionServiceFactory["getOrgPermissionByRole"] = async (role, orgId) => { - const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); - if (isCustomRole) { - const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); - if (!orgRole) - throw new NotFoundError({ - message: `Specified role '${role}' was not found in the organization with ID '${orgId}'` - }); - return { - permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]), - role: orgRole - }; - } - return { permission: buildOrgPermission([{ role, permissions: [] }]) }; - }; - - // user permission for a project in an organization - const getUserProjectPermission = async ({ - userId, - projectId, - authMethod, - userOrgId, - actionProjectType - }: TGetUserProjectPermissionArg): Promise> => { - const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); - if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" }); - - if ( - userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions) - ) { - throw new NotFoundError({ name: "The permission was not found" }); + if (type !== ActorType.USER && type !== ActorType.IDENTITY) { + throw new BadRequestError({ + message: "Invalid actor provided", + name: "Get org permission" + }); } - // If the org ID is API_KEY, the request is being made with an API Key. - // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. - - // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. - // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. - if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) { + if (orgId !== actorOrgId) { throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } + const permissionData = await permissionDAL.getPermission({ + scopeData: { + scope: AccessScope.Organization, + orgId + }, + actorId: id, + actorType: type + }); + if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); + + const permissionFromRoles = permissionData.flatMap((membership) => { + const activeRoles = membership?.roles + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ role, permissions }) => ({ role, permissions })); + const activeAdditionalPrivileges = membership?.additionalPrivileges + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ permissions }) => ({ role: OrgMembershipRole.Custom, permissions })); + return activeRoles.concat(activeAdditionalPrivileges); + }); + + const hasRole = (role: string) => + permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); + validateOrgSSO( authMethod, - userProjectPermission.orgAuthEnforced, - userProjectPermission.orgGoogleSsoAuthEnforced, - userProjectPermission.bypassOrgAuthEnabled, - userProjectPermission.orgRole + permissionData?.[0].orgAuthEnforced, + Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), + Boolean(permissionData?.[0].bypassOrgAuthEnabled), + hasRole(OrgMembershipRole.Admin) ); - if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { - throw new BadRequestError({ - message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` - }); - } - - // join two permissions and pass to build the final permission set - const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; - const additionalPrivileges = - userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ - role: ProjectMembershipRole.Custom, - permissions - })) || []; - - const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); - const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); - const unescapedMetadata = objectify( - userProjectPermission.metadata, - (i) => i.key, - (i) => i.value - ); - const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); - requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata }); - const interpolateRules = templatedRules( - { - identity: { - id: userProjectPermission.userId, - username: userProjectPermission.username, - metadata: metadataKeyValuePair - } - }, - { data: false } - ); - const permission = createMongoAbility( - JSON.parse(interpolateRules) as RawRuleOf>[], - { - conditionsMatcher - } - ); + const permission = createMongoAbility(buildOrgPermissionRules(permissionFromRoles), { + conditionsMatcher + }); return { permission, - membership: userProjectPermission, - hasRole: (role: string) => - userProjectPermission.roles.findIndex( - ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug - ) !== -1 - }; - }; - - const getIdentityProjectPermission = async ({ - identityId, - projectId, - identityOrgId, - actionProjectType - }: TGetIdentityProjectPermissionArg): Promise> => { - const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); - if (!identityProjectPermission) - throw new ForbiddenRequestError({ - name: `Identity is not a member of the specified project with ID '${projectId}'` - }); - - if ( - identityProjectPermission.roles.some( - ({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions - ) - ) { - throw new NotFoundError({ name: "Custom permission not found" }); - } - - if (identityProjectPermission.orgId !== identityOrgId) { - throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" }); - } - - if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) { - throw new BadRequestError({ - message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` - }); - } - - const rolePermissions = - identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; - const additionalPrivileges = - identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ - role: ProjectMembershipRole.Custom, - permissions - })) || []; - - const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); - const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); - const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo"); - const unescapedMetadata = objectify( - identityProjectPermission.metadata, - (i) => i.key, - (i) => i.value - ); - const identityAuthInfo = - unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo - ? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth") - : {}; - const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); - - requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo }); - const interpolateRules = templatedRules( - { - identity: { - id: identityProjectPermission.identityId, - username: identityProjectPermission.username, - metadata: metadataKeyValuePair, - auth: identityAuthInfo - } - }, - { data: false } - ); - const permission = createMongoAbility( - JSON.parse(interpolateRules) as RawRuleOf>[], - { - conditionsMatcher - } - ); - - return { - permission, - membership: identityProjectPermission, - hasRole: (role: string) => - identityProjectPermission.roles.findIndex( - ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug - ) !== -1 + memberships: permissionData, + hasRole }; }; @@ -455,10 +258,6 @@ export const permissionServiceFactory = ({ if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" }); - if (serviceTokenProject.orgId !== actorOrgId) { - throw new ForbiddenRequestError({ message: "Service token not a part of the specified organization" }); - } - if (serviceToken.projectId !== projectId) { throw new ForbiddenRequestError({ name: `Service token not a part of the specified project with ID ${projectId}` @@ -480,34 +279,156 @@ export const permissionServiceFactory = ({ const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), - membership: { - shouldUseNewPrivilegeSystem: true - } + memberships: [], + hasRole: () => false }; }; - type TProjectPermissionRT = T extends ActorType.SERVICE - ? { - permission: MongoAbility; - membership: { - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (arg: string) => boolean; - } // service token doesn't have both membership and roles - : { - permission: MongoAbility; - membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & { - orgAuthEnforced: boolean | null | undefined; - orgId: string; - roles: Array<{ role: string }>; - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (role: string) => boolean; - }; + const getProjectPermission: TPermissionServiceFactory["getProjectPermission"] = async ({ + actor: inputActor, + actorId: inputActorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType + }) => { + let actor = inputActor; + let actorId = inputActorId; - const getProjectPermissions: TPermissionServiceFactory["getProjectPermissions"] = async (projectId) => { + if (actor === ActorType.SERVICE) { + return getServiceTokenProjectPermission({ + serviceTokenId: actorId, + projectId, + actorOrgId, + actionProjectType + }); + } + + const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails"); + if ( + assumedPrivilegeDetailsCtx && + actor === ActorType.USER && + actorId === assumedPrivilegeDetailsCtx.requesterId && + projectId === assumedPrivilegeDetailsCtx.projectId + ) { + actor = assumedPrivilegeDetailsCtx.actorType; + actorId = assumedPrivilegeDetailsCtx.actorId; + } + + if (ActorType.USER !== actor && ActorType.IDENTITY !== actor) { + throw new BadRequestError({ + message: "Invalid actor provided", + name: "Get org permission" + }); + } + + const projectDetails = await projectDAL.findById(projectId); + if (!projectDetails) { + throw new NotFoundError({ message: `Project with ${projectId} not found` }); + } + + if (projectDetails.orgId !== actorOrgId) { + throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); + } + + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== projectDetails.type) { + throw new BadRequestError({ + message: `The project is of type ${projectDetails.type}. Operations of type ${actionProjectType} are not allowed.` + }); + } + + const permissionData = await permissionDAL.getPermission({ + scopeData: { + scope: AccessScope.Project, + orgId: projectDetails.orgId, + projectId + }, + actorId, + actorType: actor + }); + if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); + + const permissionFromRoles = permissionData.flatMap((membership) => { + const activeRoles = membership?.roles + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ role, permissions }) => ({ role, permissions })); + const activeAdditionalPrivileges = membership?.additionalPrivileges + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ permissions }) => ({ role: ProjectMembershipRole.Custom, permissions })); + return activeRoles.concat(activeAdditionalPrivileges); + }); + + const hasRole = (role: string) => + permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); + + validateOrgSSO( + actorAuthMethod, + permissionData?.[0].orgAuthEnforced, + Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), + Boolean(permissionData?.[0].bypassOrgAuthEnabled), + hasRole(ProjectMembershipRole.Admin) + ); + + const rules = buildProjectPermissionRules(permissionFromRoles); + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const unescapedMetadata = objectify( + permissionData?.[0]?.metadata, + (i) => i.key, + (i) => i.value + ); + const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); + requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata }); + + let username = ""; + if (actor === ActorType.USER) { + const userDetails = await userDAL.findById(actorId); + username = userDetails?.username; + } else { + const identityDetails = await identityDAL.findById(actorId); + username = identityDetails?.name; + } + + const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo"); + const identityAuthInfo = + unescapedIdentityAuthInfo?.identityId === actorId && unescapedIdentityAuthInfo + ? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth") + : {}; + + const interpolateRules = templatedRules( + { + identity: { + id: actorId, + username, + metadata: metadataKeyValuePair, + auth: identityAuthInfo + } + }, + { data: false } + ); + + const permission = createMongoAbility( + JSON.parse(interpolateRules) as RawRuleOf>[], + { + conditionsMatcher + } + ); + + return { + permission, + memberships: permissionData, + hasRole + }; + }; + + const getProjectPermissions: TPermissionServiceFactory["getProjectPermissions"] = async (projectId, orgId) => { // fetch user permissions - const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId); + const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId, orgId); const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => { const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; @@ -547,13 +468,12 @@ export const permissionServiceFactory = ({ return { permission, id: userProjectPermission.userId, - name: userProjectPermission.username, - membershipId: userProjectPermission.id + name: userProjectPermission.username }; }); // fetch identity permissions - const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId); + const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId, orgId); const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => { const rolePermissions = identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; @@ -593,8 +513,7 @@ export const permissionServiceFactory = ({ return { permission, id: identityProjectPermission.identityId, - name: identityProjectPermission.username, - membershipId: identityProjectPermission.id + name: identityProjectPermission.username }; }); @@ -611,8 +530,7 @@ export const permissionServiceFactory = ({ return { permission, id: groupProjectPermission.groupId, - name: groupProjectPermission.username, - membershipId: groupProjectPermission.id + name: groupProjectPermission.username }; }); @@ -623,141 +541,105 @@ export const permissionServiceFactory = ({ }; }; - const getProjectPermission = async ({ - actor: inputActor, - actorId: inputActorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType - }: TGetProjectPermissionArg): Promise> => { - let actor = inputActor; - let actorId = inputActorId; - const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails"); - if ( - assumedPrivilegeDetailsCtx && - actor === ActorType.USER && - actorId === assumedPrivilegeDetailsCtx.requesterId && - projectId === assumedPrivilegeDetailsCtx.projectId - ) { - actor = assumedPrivilegeDetailsCtx.actorType; - actorId = assumedPrivilegeDetailsCtx.actorId; + // instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like + // admin member or user defined ones like biller etc + const getOrgPermissionByRoles: TPermissionServiceFactory["getOrgPermissionByRoles"] = async (roles, orgId) => { + const formattedRoles = roles.map((role) => ({ + name: role, + isCustom: !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole) + })); + + const customRoles = formattedRoles.filter((el) => el.isCustom).map((el) => el.name); + const customRoleDetails = customRoles.length + ? await roleDAL.find({ + orgId, + $in: { + slug: customRoles + } + }) + : []; + if (customRoles.length !== customRoleDetails.length) { + const missingRoles = customRoles.filter((role) => !customRoleDetails.find((el) => el.slug === role)); + throw new NotFoundError({ + message: `Specified roles '${missingRoles.join(",")}' was not found in the organization with ID '${orgId}'` + }); } - if (actor === ActorType.SERVICE) { - return getServiceTokenProjectPermission({ - serviceTokenId: actorId, - projectId, - actorOrgId, - actionProjectType - }) as Promise>; - } - - const cachedProjectPermissionVersion = await keyStore.pgGetIntItem( - KeyStorePrefixes.ProjectPermissionDalVersion(projectId) - ); - const projectPermissionVersion = Number(cachedProjectPermissionVersion || 0); - const cacheKey = KeyStorePrefixes.ProjectPermission( - projectId, - projectPermissionVersion, - actor, - actorId, - actionProjectType || ActionProjectType.Any - ); - - try { - const cachedData = await keyStore.getItem(cacheKey); - if (cachedData) { - const parsed = JSON.parse(cachedData) as { - rules: RawRuleOf>[]; - membership: { - roles?: Array<{ role: string; customRoleSlug?: string }>; - [key: string]: unknown; - }; - }; - const permission = createMongoAbility(parsed.rules, { - conditionsMatcher - }); - + return formattedRoles.map((el) => { + if (el.isCustom) { + const roleDetails = customRoleDetails.find((role) => role.slug === el.name); return { - permission, - membership: parsed.membership, - hasRole: (role: string) => - parsed.membership.roles?.findIndex( - ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug - ) !== -1 - } as TProjectPermissionRT; + permission: createMongoAbility( + buildOrgPermissionRules([{ role: OrgMembershipRole.Custom, permissions: roleDetails?.permissions || [] }]), + { + conditionsMatcher + } + ), + role: roleDetails! + }; } - } catch (error) { - logger.error(error, "Failed to get project permission"); - } - let result: TProjectPermissionRT; - - switch (actor) { - case ActorType.USER: - result = (await getUserProjectPermission({ - userId: actorId, - projectId, - authMethod: actorAuthMethod, - userOrgId: actorOrgId, - actionProjectType - })) as TProjectPermissionRT; - break; - case ActorType.IDENTITY: - result = (await getIdentityProjectPermission({ - identityId: actorId, - projectId, - identityOrgId: actorOrgId, - actionProjectType - })) as TProjectPermissionRT; - break; - default: - throw new BadRequestError({ - message: "Invalid actor provided", - name: "Get project permission" - }); - } - - try { - const cacheData = { - rules: result.permission.rules, - membership: result.membership + return { + permission: createMongoAbility( + buildOrgPermissionRules([{ role: el.name, permissions: [] }]), + { + conditionsMatcher + } + ) }; - - const ttl = calculateProjectPermissionTtl(result.membership); - await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData)); - } catch (error) { - logger.error(error, "Failed to cache project permission"); - } - - return result; + }); }; - const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async ( - role, + const getProjectPermissionByRoles: TPermissionServiceFactory["getProjectPermissionByRoles"] = async ( + roles, projectId ) => { - const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); - if (isCustomRole) { - const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); - if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` }); - const rules = buildProjectPermissionRules([ - { role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } - ]); - return { - permission: createMongoAbility(rules, { - conditionsMatcher - }), - role: projectRole - }; + const formattedRoles = roles.map((role) => ({ + name: role, + isCustom: !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) + })); + + const customRoles = formattedRoles.filter((el) => el.isCustom).map((el) => el.name); + const customRoleDetails = customRoles.length + ? await roleDAL.find({ + projectId, + $in: { + slug: customRoles + } + }) + : []; + if (customRoles.length !== customRoleDetails.length) { + const missingRoles = customRoles.filter((role) => !customRoleDetails.find((el) => el.slug === role)); + throw new NotFoundError({ + message: `Specified roles '${missingRoles.join(",")}' was not found in the project with ID '${projectId}'` + }); } - const rules = buildProjectPermissionRules([{ role, permissions: [] }]); - const permission = createMongoAbility(rules, { - conditionsMatcher + return formattedRoles.map((el) => { + if (el.isCustom) { + const roleDetails = customRoleDetails.find((role) => role.slug === el.name); + return { + permission: createMongoAbility( + buildProjectPermissionRules([ + { role: ProjectMembershipRole.Custom, permissions: roleDetails?.permissions || [] } + ]), + { + conditionsMatcher + } + ), + role: roleDetails! + }; + } + + return { + permission: createMongoAbility( + buildProjectPermissionRules([{ role: el.name, permissions: [] }]), + { + conditionsMatcher + } + ) + }; }); - return { permission }; }; const checkGroupProjectPermission: TPermissionServiceFactory["checkGroupProjectPermission"] = async ({ @@ -785,15 +667,11 @@ export const permissionServiceFactory = ({ }; return { - getUserOrgPermission, getOrgPermission, - getUserProjectPermission, getProjectPermission, getProjectPermissions, - getOrgPermissionByRole, - getProjectPermissionByRole, - buildOrgPermission, - buildProjectPermissionRules, + getOrgPermissionByRoles, + getProjectPermissionByRoles, checkGroupProjectPermission, invalidateProjectPermissionCache }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4c7f1faac6..d2c5b30dbd 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,6 +1,7 @@ import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { z } from "zod"; +import { ProjectMembershipRole } from "@app/db/schemas"; import { CASL_ACTION_SCHEMA_ENUM, CASL_ACTION_SCHEMA_NATIVE_ENUM @@ -199,6 +200,9 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export const isCustomProjectRole = (slug: string) => + !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); + export enum ProjectPermissionSub { Role = "role", Member = "member", diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts deleted file mode 100644 index 6a3be26310..0000000000 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify, TOrmify } from "@app/lib/knex"; - -export type TProjectUserAdditionalPrivilegeDALFactory = TOrmify; - -export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient): TProjectUserAdditionalPrivilegeDALFactory => { - const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts deleted file mode 100644 index c2876572f1..0000000000 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ /dev/null @@ -1,388 +0,0 @@ -import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; - -import { ActionProjectType, TableName } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; - -import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal"; -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { - ProjectPermissionMemberActions, - ProjectPermissionSet, - ProjectPermissionSub -} from "../permission/project-permission"; -import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; -import { - ProjectUserAdditionalPrivilegeTemporaryMode, - TProjectUserAdditionalPrivilegeServiceFactory -} from "./project-user-additional-privilege-types"; - -type TProjectUserAdditionalPrivilegeServiceFactoryDep = { - projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; - projectMembershipDAL: Pick; - permissionService: Pick; - accessApprovalRequestDAL: Pick; -}; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -export const projectUserAdditionalPrivilegeServiceFactory = ({ - projectUserAdditionalPrivilegeDAL, - projectMembershipDAL, - permissionService, - accessApprovalRequestDAL -}: TProjectUserAdditionalPrivilegeServiceFactoryDep): TProjectUserAdditionalPrivilegeServiceFactory => { - const create: TProjectUserAdditionalPrivilegeServiceFactory["create"] = async ({ - slug, - actor, - actorId, - permissions: customPermission, - actorOrgId, - actorAuthMethod, - projectMembershipId, - ...dto - }) => { - const projectMembership = await projectMembershipDAL.findById(projectMembershipId); - if (!projectMembership) - throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: projectMembership.userId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - targetUserPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged user", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ - slug, - projectId: projectMembership.projectId, - userId: projectMembership.userId - }); - if (existingSlug) - throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` }); - - validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ - userId: projectMembership.userId, - projectId: projectMembership.projectId, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ - projectId: projectMembership.projectId, - userId: projectMembership.userId, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateById: TProjectUserAdditionalPrivilegeServiceFactory["updateById"] = async ({ - privilegeId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - ...dto - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: projectMembership.userId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - targetUserPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged user", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - if (dto?.slug) { - const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ - slug: dto.slug, - userId: projectMembership.id, - projectId: projectMembership.projectId - }); - if (existingSlug && existingSlug.id !== userPrivilege.id) - throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` }); - } - - validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(dto.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; - - const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions)); - if (isTemporary) { - const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime; - const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange; - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - slug: dto.slug, - permissions: packedPermission, - isTemporary: dto.isTemporary, - temporaryRange: dto.temporaryRange, - temporaryMode: dto.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - slug: dto.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteById: TProjectUserAdditionalPrivilegeServiceFactory["deleteById"] = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - privilegeId - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - - await accessApprovalRequestDAL.update( - { - privilegeId: userPrivilege.id - }, - { - privilegeDeletedAt: new Date(), - status: ApprovalStatus.REJECTED - } - ); - const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsById: TProjectUserAdditionalPrivilegeServiceFactory["getPrivilegeDetailsById"] = async ({ - privilegeId, - actorOrgId, - actor, - actorId, - actorAuthMethod - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - - return { - ...userPrivilege, - permissions: unpackPermissions(userPrivilege.permissions) - }; - }; - - const listPrivileges: TProjectUserAdditionalPrivilegeServiceFactory["listPrivileges"] = async ({ - projectMembershipId, - actorOrgId, - actor, - actorId, - actorAuthMethod - }) => { - const projectMembership = await projectMembershipDAL.findById(projectMembershipId); - if (!projectMembership) - throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - - const userPrivileges = await projectUserAdditionalPrivilegeDAL.find( - { - userId: projectMembership.userId, - projectId: projectMembership.projectId - }, - { sort: [[`${TableName.ProjectUserAdditionalPrivilege}.slug` as "slug", "asc"]] } - ); - return userPrivileges; - }; - - return { - create, - updateById, - deleteById, - getPrivilegeDetailsById, - listPrivileges - }; -}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts deleted file mode 100644 index a700d997dc..0000000000 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TProjectUserAdditionalPrivilege } from "@app/db/schemas"; -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum ProjectUserAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateUserPrivilegeDTO = ( - | { - permissions: TProjectPermissionV2Schema[]; - projectMembershipId: string; - slug: string; - isTemporary: false; - } - | { - permissions: TProjectPermissionV2Schema[]; - projectMembershipId: string; - slug: string; - isTemporary: true; - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit & - Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - -export type TDeleteUserPrivilegeDTO = Omit & { privilegeId: string }; - -export type TGetUserPrivilegeDetailsDTO = Omit & { privilegeId: string }; - -export type TListUserPrivilegesDTO = Omit & { projectMembershipId: string }; - -interface TAdditionalPrivilege extends TProjectUserAdditionalPrivilege { - permissions: { - action: string[]; - subject?: string | undefined; - conditions?: unknown; - inverted?: boolean | undefined; - }[]; -} - -export type TProjectUserAdditionalPrivilegeServiceFactory = { - create: (arg: TCreateUserPrivilegeDTO) => Promise; - updateById: (arg: TUpdateUserPrivilegeDTO) => Promise; - deleteById: (arg: TDeleteUserPrivilegeDTO) => Promise; - getPrivilegeDetailsById: (arg: TGetUserPrivilegeDetailsDTO) => Promise; - listPrivileges: (arg: TListUserPrivilegesDTO) => Promise; -}; diff --git a/backend/src/ee/services/relay/relay-dal.ts b/backend/src/ee/services/relay/relay-dal.ts index 9107e0807c..8e7eac8de4 100644 --- a/backend/src/ee/services/relay/relay-dal.ts +++ b/backend/src/ee/services/relay/relay-dal.ts @@ -1,11 +1,47 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TRelays } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { buildFindFilter, ormify, TFindFilter, TFindOpt } from "@app/lib/knex"; export type TRelayDALFactory = ReturnType; export const relayDalFactory = (db: TDbClient) => { const orm = ormify(db, TableName.Relay); - return orm; + const find = async ( + filter: TFindFilter & { isHeartbeatStale?: boolean }, + { offset, limit, sort, tx }: TFindOpt = {} + ) => { + try { + const { isHeartbeatStale, ...regularFilter } = filter; + + const query = (tx || db.replicaNode())(TableName.Relay) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(regularFilter, TableName.Relay)); + + if (isHeartbeatStale) { + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + void query.whereNotNull(`${TableName.Relay}.heartbeat`); + void query.where(`${TableName.Relay}.heartbeat`, "<", oneHourAgo); + void query.where((v) => { + void v + .whereNull(`${TableName.Relay}.healthAlertedAt`) + .orWhere(`${TableName.Relay}.healthAlertedAt`, "<", db.ref("heartbeat").withSchema(TableName.Relay)); + }); + } + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + return docs; + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.Relay}: Find` }); + } + }; + + return { ...orm, find }; }; diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 696bd1f4f8..41fe91bfc1 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -2,11 +2,15 @@ import { isIP } from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { CronJob } from "cron"; -import { TRelays } from "@app/db/schemas"; +import { OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; +import { createRelayConnection } from "@app/lib/gateway-v2/gateway-v2"; +import { logger } from "@app/lib/logger"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; @@ -16,6 +20,11 @@ import { } from "@app/services/certificate-authority/certificate-authority-fns"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns"; import { TLicenseServiceFactory } from "../license/license-service"; @@ -39,7 +48,11 @@ export const relayServiceFactory = ({ relayDAL, kmsService, licenseService, - permissionService + permissionService, + orgDAL, + notificationService, + smtpService, + userDAL }: { instanceRelayConfigDAL: TInstanceRelayConfigDALFactory; orgRelayConfigDAL: TOrgRelayConfigDALFactory; @@ -47,6 +60,10 @@ export const relayServiceFactory = ({ kmsService: TKmsServiceFactory; licenseService: TLicenseServiceFactory; permissionService: TPermissionServiceFactory; + orgDAL: Pick; + notificationService: Pick; + smtpService: Pick; + userDAL: Pick; }) => { const $getInstanceCAs = async () => { const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => { @@ -1056,6 +1073,78 @@ export const relayServiceFactory = ({ }); }; + const heartbeat = async ({ + name, + identityId, + actorAuthMethod, + orgId + }: { + name: string; + identityId?: string; + actorAuthMethod?: ActorAuthMethod; + orgId?: string; + }) => { + const relay = await relayDAL.findOne({ + name, + orgId: orgId ?? null + }); + + if (!relay) { + throw new NotFoundError({ message: `Relay with name ${name} not found.` }); + } + + let clientOrgId: string; + let clientOrgName: string; + + if (relay.orgId) { + if (!identityId || !orgId || relay.orgId !== orgId) { + throw new ForbiddenRequestError({ + message: "You do not have permission to perform this action on this relay." + }); + } + + const { permission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityId, + orgId, + actorAuthMethod!, + orgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionRelayActions.CreateRelays, + OrgPermissionSubjects.Relay + ); + clientOrgId = orgId; + clientOrgName = orgId; + } else { + clientOrgId = "00000000-0000-0000-0000-000000000000"; + clientOrgName = "heartbeat"; + } + + const relayClientCredentials = await getCredentialsForClient({ + relayId: relay.id, + orgId: clientOrgId, + orgName: clientOrgName, + gatewayId: "00000000-0000-0000-0000-000000000000", + gatewayName: "heartbeat", + duration: 60 * 1000 // 1 minute + }); + + try { + await createRelayConnection({ + relayHost: relayClientCredentials.relayHost, + clientCertificate: relayClientCredentials.clientCertificate, + clientPrivateKey: relayClientCredentials.clientPrivateKey, + serverCertificateChain: relayClientCredentials.serverCertificateChain + }); + + await relayDAL.updateById(relay.id, { heartbeat: new Date() }); + } catch (err) { + const error = err as Error; + throw new BadRequestError({ message: `Relay ${name} is not reachable: ${error.message}` }); + } + }; + const getRelays = async ({ actorId, actor, @@ -1120,11 +1209,99 @@ export const relayServiceFactory = ({ return deletedRelay; }; + const $healthcheckNotify = async () => { + const unhealthyRelays = await relayDAL.find({ + isHeartbeatStale: true + }); + + if (unhealthyRelays.length === 0) return; + + logger.warn( + { relayIds: unhealthyRelays.map((g) => g.id) }, + "Found relays with last heartbeat over an hour ago. Sending notifications." + ); + + const relaysByOrg = groupBy(unhealthyRelays, (r) => r.orgId ?? "instance"); + + for await (const [orgId, relays] of Object.entries(relaysByOrg)) { + try { + if (orgId === "instance") { + const superAdmins = await userDAL.find({ + superAdmin: true + }); + + const recipients = superAdmins.map((admin) => admin.email).filter((v): v is string => !!v); + + if (recipients.length > 0) { + const relayNames = relays.map((r) => `"${r.name}"`).join(", "); + await smtpService.sendMail({ + recipients, + subjectLine: "Relay Health Alert", + substitutions: { + type: "instance-relay", + names: relayNames + }, + template: SmtpTemplates.HealthAlert + }); + } + } else { + const admins = await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin); + if (admins.length === 0) { + // eslint-disable-next-line no-continue + continue; + } + + const relayNames = relays.map((r) => `"${r.name}"`).join(", "); + const body = `The following relay(s) in your organization may be offline as they haven't reported a heartbeat in over an hour: ${relayNames}. Please check their status.`; + + await notificationService.createUserNotifications( + admins.map((admin) => ({ + userId: admin.user.id, + orgId, + type: NotificationType.RELAY_HEALTH_ALERT, + title: "Relay Health Alert", + body, + link: "/organization/networking" + })) + ); + + await smtpService.sendMail({ + recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v), + subjectLine: "Relay Health Alert", + substitutions: { + type: "relay", + names: relayNames + }, + template: SmtpTemplates.HealthAlert + }); + } + + await Promise.all(relays.map((r) => relayDAL.updateById(r.id, { healthAlertedAt: new Date() }))); + } catch (error) { + logger.error(error, `Failed to send relay health notifications for organization [orgId=${orgId}]`); + } + } + }; + + const initializeHealthcheckNotify = async () => { + logger.info("Setting up background notification process for relay health-checks"); + + await $healthcheckNotify(); + + // run every 5 minutes + const job = new CronJob("*/5 * * * *", $healthcheckNotify); + job.start(); + + return job; + }; + return { registerRelay, getCredentialsForGateway, getCredentialsForClient, getRelays, - deleteRelay + deleteRelay, + heartbeat, + initializeHealthcheckNotify }; }; 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 f1ee313e86..ab84ebd398 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -4,6 +4,7 @@ import { Knex } from "knex"; import RE2 from "re2"; import { + AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -19,13 +20,13 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { 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 { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; 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"; @@ -68,32 +69,30 @@ type TSamlConfigServiceFactoryDep = { "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; identityMetadataDAL: Pick; - orgMembershipDAL: Pick; - groupDAL: Pick; - userGroupMembershipDAL: Pick< - TUserGroupMembershipDALFactory, - "find" | "delete" | "transaction" | "insertMany" | "filterProjectsByUserMembership" - >; - groupProjectDAL: Pick; - projectDAL: Pick; - projectBotDAL: Pick; - projectKeyDAL: Pick; + membershipRoleDAL: Pick; permissionService: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; kmsService: Pick; + userGroupMembershipDAL: Pick< + TUserGroupMembershipDALFactory, + "find" | "delete" | "transaction" | "insertMany" | "filterProjectsByUserMembership" + >; + groupDAL: Pick; + projectDAL: Pick; + projectBotDAL: Pick; + projectKeyDAL: Pick; + membershipGroupDAL: Pick; }; export const samlConfigServiceFactory = ({ samlConfigDAL, orgDAL, - orgMembershipDAL, userDAL, userAliasDAL, groupDAL, userGroupMembershipDAL, - groupProjectDAL, projectDAL, projectBotDAL, projectKeyDAL, @@ -102,7 +101,9 @@ export const samlConfigServiceFactory = ({ tokenService, smtpService, identityMetadataDAL, - kmsService + kmsService, + membershipRoleDAL, + membershipGroupDAL }: TSamlConfigServiceFactoryDep): TSamlConfigServiceFactory => { const parseSamlGroups = (groupsValue: string): string[] => { let samlGroups: string[] = []; @@ -195,10 +196,10 @@ export const samlConfigServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx: transaction }); } catch (error) { @@ -218,7 +219,7 @@ export const samlConfigServiceFactory = ({ group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx: transaction }); @@ -506,26 +507,35 @@ export const samlConfigServiceFactory = ({ 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 + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); + if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: userAlias.userId, + actorUserId: userAlias.userId, inviteEmail: email, - orgId, - role, - roleId, - status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, + scopeOrgId: orgId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + 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( @@ -606,8 +616,9 @@ export const samlConfigServiceFactory = ({ const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -617,15 +628,22 @@ export const samlConfigServiceFactory = ({ const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: newUser.id, - inviteEmail: email, - orgId, - role, - roleId, + actorUserId: newUser.id, + scopeOrgId: orgId, + scope: AccessScope.Organization, 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 - isActive: true + isActive: true, + inviteEmail: email.toLowerCase() + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId }, tx ); diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 9cc6e134df..a08cd5dbfc 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -2,7 +2,15 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { scimPatch } from "scim-patch"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas"; +import { + AccessScope, + OrgMembershipRole, + OrgMembershipStatus, + TableName, + TGroups, + TMemberships, + TUsers +} from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -11,18 +19,19 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; +import { TMembershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; -import { deleteOrgMembershipFn } from "@app/services/org/org-fns"; +import { deleteOrgMembershipsFn } from "@app/services/org/org-fns"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { OrgAuthMethod } from "@app/services/org/org-types"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; 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 { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -33,7 +42,6 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns"; import { TScimGroup, TScimServiceFactory } from "./scim-types"; @@ -55,12 +63,8 @@ type TScimServiceFactoryDep = { | "updateMembershipById" | "findOrgById" >; - orgMembershipDAL: Pick< - TOrgMembershipDALFactory, - "find" | "findOne" | "create" | "updateById" | "findById" | "update" - >; + membershipUserDAL: TMembershipUserDALFactory; projectDAL: Pick; - projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, | "create" @@ -72,7 +76,8 @@ type TScimServiceFactoryDep = { | "updateById" | "update" >; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: TMembershipRoleDALFactory; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, | "find" @@ -88,8 +93,8 @@ type TScimServiceFactoryDep = { licenseService: Pick; permissionService: Pick; smtpService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; }; export const scimServiceFactory = ({ @@ -98,18 +103,18 @@ export const scimServiceFactory = ({ userDAL, userAliasDAL, orgDAL, - orgMembershipDAL, projectDAL, - projectMembershipDAL, groupDAL, - groupProjectDAL, userGroupMembershipDAL, projectKeyDAL, projectBotDAL, permissionService, - projectUserAdditionalPrivilegeDAL, smtpService, - externalGroupOrgRoleMappingDAL + externalGroupOrgRoleMappingDAL, + membershipGroupDAL, + membershipUserDAL, + membershipRoleDAL, + additionalPrivilegeDAL }: TScimServiceFactoryDep): TScimServiceFactory => { const createScimToken: TScimServiceFactory["createScimToken"] = async ({ actor, @@ -244,8 +249,9 @@ export const scimServiceFactory = ({ const getScimUser: TScimServiceFactory["getScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -322,13 +328,14 @@ export const scimServiceFactory = ({ const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => { let user: TUsers | undefined; - let orgMembership: TOrgMemberships; + let orgMembership: TMemberships; if (userAlias) { user = await userDAL.findById(userAlias.userId, tx); - orgMembership = await orgMembershipDAL.findOne( + orgMembership = await membershipUserDAL.findOne( { - userId: user.id, - orgId + actorUserId: user.id, + scope: AccessScope.Organization, + scopeOrgId: orgId }, tx ); @@ -336,20 +343,27 @@ export const scimServiceFactory = ({ if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); - orgMembership = await orgMembershipDAL.create( + orgMembership = await membershipUserDAL.create( { - userId: userAlias.userId, + actorUserId: userAlias.userId, inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: orgMembership.id, + role, + customRoleId: roleId + }, + tx + ); } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { - orgMembership = await orgMembershipDAL.updateById( + orgMembership = await membershipUserDAL.updateById( orgMembership.id, { status: OrgMembershipStatus.Accepted @@ -401,8 +415,9 @@ export const scimServiceFactory = ({ const [foundOrgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -412,18 +427,25 @@ export const scimServiceFactory = ({ if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); - orgMembership = await orgMembershipDAL.create( + orgMembership = await membershipUserDAL.create( { - userId: user.id, + actorUserId: user.id, inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: orgMembership.id, + role, + customRoleId: roleId + }, + tx + ); // Only update the membership to Accepted if the user account is already completed. } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { orgMembership = await orgDAL.updateMembershipById( @@ -475,8 +497,9 @@ export const scimServiceFactory = ({ const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -485,7 +508,7 @@ export const scimServiceFactory = ({ }); }); - if (!membership) + if (!membership || !membership.actorUserId) throw new ScimRequestError({ detail: "User not found", status: 404 @@ -514,7 +537,7 @@ export const scimServiceFactory = ({ org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails; await userDAL.transaction(async (tx) => { - await orgMembershipDAL.updateById( + await membershipUserDAL.updateById( membership.id, { isActive: scimUser.active @@ -523,7 +546,7 @@ export const scimServiceFactory = ({ ); const hasEmailChanged = scimUser.emails[0].value !== membership.email; await userDAL.updateById( - membership.userId, + membership.actorUserId as string, { firstName: scimUser.name.givenName, email: scimUser.emails[0].value.toLowerCase(), @@ -556,8 +579,9 @@ export const scimServiceFactory = ({ const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -566,7 +590,7 @@ export const scimServiceFactory = ({ }); }); - if (!membership) + if (!membership || !membership.actorUserId) throw new ScimRequestError({ detail: "User not found", status: 404 @@ -587,7 +611,7 @@ export const scimServiceFactory = ({ { orgId, aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML, - userId: membership.userId + userId: membership.actorUserId as string }, { externalId @@ -595,7 +619,7 @@ export const scimServiceFactory = ({ tx ); - await orgMembershipDAL.updateById( + await membershipUserDAL.updateById( membership.id, { isActive: active @@ -603,7 +627,7 @@ export const scimServiceFactory = ({ tx ); await userDAL.updateById( - membership.userId, + membership.actorUserId!, { firstName, email: email?.toLowerCase(), @@ -628,8 +652,9 @@ export const scimServiceFactory = ({ const deleteScimUser: TScimServiceFactory["deleteScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!membership) @@ -645,15 +670,17 @@ export const scimServiceFactory = ({ }); } - await deleteOrgMembershipFn({ - orgMembershipId: membership.id, - orgId: membership.orgId, + await deleteOrgMembershipsFn({ + orgMembershipIds: [membership.id], + orgId: membership.scopeOrgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, - licenseService + licenseService, + membershipUserDAL, + membershipRoleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); return {}; // intentionally return empty object upon success @@ -750,8 +777,9 @@ export const scimServiceFactory = ({ if (!externalGroupMapping) return; // only get org memberships that are new (invites) - const newOrgMemberships = await orgMembershipDAL.find({ + const newOrgMemberships = await membershipUserDAL.find({ status: "invited", + scope: AccessScope.Organization, $in: { id: members.map((member) => member.value) } @@ -760,15 +788,15 @@ export const scimServiceFactory = ({ if (!newOrgMemberships.length) return; // set new membership roles to group mapping value - await orgMembershipDAL.update( + await membershipRoleDAL.update( { $in: { - id: newOrgMemberships.map((membership) => membership.id) + membershipId: newOrgMemberships.map((membership) => membership.id) } }, { role: externalGroupMapping.role, - roleId: externalGroupMapping.roleId + customRoleId: externalGroupMapping.roleId } ); }; @@ -821,8 +849,26 @@ export const scimServiceFactory = ({ tx ); + const groupMembership = await membershipGroupDAL.create( + { + scope: AccessScope.Organization, + actorGroupId: group.id, + scopeOrgId: orgId + }, + tx + ); + + await membershipRoleDAL.create( + { + membershipId: groupMembership.id, + role: OrgMembershipRole.NoAccess + }, + tx + ); + if (members && members.length) { - const orgMemberships = await orgMembershipDAL.find({ + const orgMemberships = await membershipUserDAL.find({ + scope: AccessScope.Organization, $in: { id: members.map((member) => member.value) } @@ -830,14 +876,14 @@ export const scimServiceFactory = ({ const newMembers = await addUsersToGroupByUserIds({ group, - userIds: orgMemberships.map((membership) => membership.userId as string), + userIds: orgMemberships.map((membership) => membership.actorUserId as string), userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); @@ -850,9 +896,10 @@ export const scimServiceFactory = ({ }); const orgMemberships = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id) + [`${TableName.Membership}.actorUserId` as "actorUserId"]: newGroup.newMembers.map((member) => member.id) } }); @@ -895,9 +942,10 @@ export const scimServiceFactory = ({ .then((g) => g.members); const orgMemberships = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: users + [`${TableName.Membership}.actorUserId` as "actorUserId"]: users .filter((user) => user.isPartOfGroup) .map((user) => user.id) } @@ -933,10 +981,10 @@ export const scimServiceFactory = ({ } const updatedGroup = await groupDAL.transaction(async (tx) => { - if (group.name !== displayName) { + if (group?.name !== displayName) { await externalGroupOrgRoleMappingDAL.update( { - groupName: group.name, + groupName: group?.name, orgId }, { @@ -958,14 +1006,16 @@ export const scimServiceFactory = ({ } const orgMemberships = members.length - ? await orgMembershipDAL.find({ + ? await membershipUserDAL.find({ + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { id: members.map((member) => member.value) } }) : []; - const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId)); + const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.actorUserId as string)); const userGroupMembers = await userGroupMembershipDAL.find({ groupId: group.id }); @@ -978,20 +1028,20 @@ export const scimServiceFactory = ({ const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds); const allMembersUserIdsSet = new Set(allMembersUserIds); - const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string)); + const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.actorUserId as string)); const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId)); if (toAddUserIds.length) { await addUsersToGroupByUserIds({ group, - userIds: toAddUserIds.map((member) => member.userId as string), + userIds: toAddUserIds.map((member) => member.actorUserId as string), userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); } @@ -1002,7 +1052,7 @@ export const scimServiceFactory = ({ userIds: toRemoveUserIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx }); 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 01caef2238..7597dcfd4e 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 @@ -2,9 +2,10 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, SecretApprovalRequestsSchema, TableName, - TOrgMemberships, + TMemberships, TSecretApprovalRequests, TSecretApprovalRequestsSecrets, TUserGroupMembership, @@ -36,6 +37,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .where(filter) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .join( TableName.SecretApprovalPolicy, `${TableName.SecretApprovalRequest}.policyId`, @@ -109,24 +111,22 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `secretApprovalReviewerUser.id` ) - .leftJoin( - db(TableName.OrgMembership).as("approverOrgMembership"), - `${TableName.SecretApprovalPolicyApprover}.approverUserId`, - `approverOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("approverGroupOrgMembership"), - `secretApprovalPolicyGroupApproverUser.id`, - `approverGroupOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("reviewerOrgMembership"), - `${TableName.SecretApprovalRequestReviewer}.reviewerUserId`, - `reviewerOrgMembership.userId` - ) + .leftJoin(db(TableName.Membership).as("approverOrgMembership"), (qb) => { + qb.on(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, `approverOrgMembership.actorUserId`) + .andOn(`approverOrgMembership.scopeOrgId`, `${TableName.Project}.orgId`) + .andOn(`approverOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) + .leftJoin(db(TableName.Membership).as("approverGroupOrgMembership"), (qb) => { + qb.on(`secretApprovalPolicyGroupApproverUser.id`, `approverGroupOrgMembership.actorUserId`) + .andOn(`approverGroupOrgMembership.scopeOrgId`, `${TableName.Project}.orgId`) + .andOn(`approverGroupOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) + .leftJoin(db(TableName.Membership).as("reviewerOrgMembership"), (qb) => { + qb.on(`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`, `reviewerOrgMembership.actorUserId`) + .andOn(`reviewerOrgMembership.scopeOrgId`, `${TableName.Project}.orgId`) + .andOn(`reviewerOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) .select(selectAllTableCols(TableName.SecretApprovalRequest)) .select( tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), diff --git a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts index aa6d4f66ad..0846532f9e 100644 --- a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts +++ b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts @@ -44,10 +44,7 @@ type TSshHostGroupServiceFactoryDep = { sshHostLoginUserDAL: Pick; sshHostLoginUserMappingDAL: Pick; userDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission" - >; + permissionService: Pick; licenseService: Pick; groupDAL: Pick; }; diff --git a/backend/src/ee/services/ssh-host/ssh-host-fns.ts b/backend/src/ee/services/ssh-host/ssh-host-fns.ts index dec15e093f..7e188a5fe0 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-fns.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-fns.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "../permission/project-permission"; import { TCreateSshLoginMappingsDTO } from "./ssh-host-types"; @@ -59,11 +60,12 @@ export const createSshLoginMappings = async ({ for await (const user of users) { // check that each user has access to the SSH project - await permissionService.getUserProjectPermission({ - userId: user.id, + await permissionService.getProjectPermission({ + actor: ActorType.USER, + actorId: user.id, projectId, - authMethod: actorAuthMethod, - userOrgId: actorOrgId, + actorAuthMethod, + actorOrgId, actionProjectType: ActionProjectType.SSH }); } diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index d1082b4df7..37d843c479 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -64,10 +64,7 @@ type TSshHostServiceFactoryDep = { >; sshHostLoginUserDAL: TSshHostLoginUserDALFactory; sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission" - >; + permissionService: Pick; kmsService: Pick; }; diff --git a/backend/src/ee/services/ssh-host/ssh-host-types.ts b/backend/src/ee/services/ssh-host/ssh-host-types.ts index a8269ac375..698f067f26 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-types.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-types.ts @@ -66,7 +66,7 @@ type BaseCreateSshLoginMappingsDTO = { sshHostLoginUserDAL: Pick; sshHostLoginUserMappingDAL: Pick; userDAL: Pick; - permissionService: Pick; + permissionService: Pick; groupDAL: Pick; projectId: string; actorAuthMethod: ActorAuthMethod; diff --git a/backend/src/lib/fn/object.ts b/backend/src/lib/fn/object.ts index 65d0b78594..6ff7278aae 100644 --- a/backend/src/lib/fn/object.ts +++ b/backend/src/lib/fn/object.ts @@ -53,3 +53,53 @@ export const titleCaseToCamelCase = (obj: unknown): unknown => { return result; }; + +export const deepEqual = (obj1: unknown, obj2: unknown): boolean => { + if (obj1 === obj2) return true; + + if (obj1 === null || obj2 === null || obj1 === undefined || obj2 === undefined) { + return obj1 === obj2; + } + + if (typeof obj1 !== typeof obj2) return false; + + if (typeof obj1 !== "object") return obj1 === obj2; + + if (Array.isArray(obj1) !== Array.isArray(obj2)) return false; + + if (Array.isArray(obj1)) { + const arr1 = obj1 as unknown[]; + const arr2 = obj2 as unknown[]; + if (arr1.length !== arr2.length) return false; + return arr1.every((val, idx) => deepEqual(val, arr2[idx])); + } + + const keys1 = Object.keys(obj1 as Record).sort(); + const keys2 = Object.keys(obj2 as Record).sort(); + + if (keys1.length !== keys2.length) return false; + if (keys1.some((key, idx) => key !== keys2[idx])) return false; + + return keys1.every((key) => + deepEqual((obj1 as Record)[key], (obj2 as Record)[key]) + ); +}; + +export const deepEqualSkipFields = (obj1: unknown, obj2: unknown, skipFields: string[] = []): boolean => { + if (skipFields.length === 0) { + return deepEqual(obj1, obj2); + } + + if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null) { + return deepEqual(obj1, obj2); + } + + const filtered1 = Object.fromEntries( + Object.entries(obj1 as Record).filter(([key]) => !skipFields.includes(key)) + ); + const filtered2 = Object.fromEntries( + Object.entries(obj2 as Record).filter(([key]) => !skipFields.includes(key)) + ); + + return deepEqual(filtered1, filtered2); +}; diff --git a/backend/src/lib/gateway-v2/gateway-v2.ts b/backend/src/lib/gateway-v2/gateway-v2.ts index e6e873f113..5ae0e5b1d9 100644 --- a/backend/src/lib/gateway-v2/gateway-v2.ts +++ b/backend/src/lib/gateway-v2/gateway-v2.ts @@ -18,7 +18,7 @@ interface IGatewayRelayServer { getRelayError: () => string; } -const createRelayConnection = async ({ +export const createRelayConnection = async ({ relayHost, clientCertificate, clientPrivateKey, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 406daa63d6..01de9d5592 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -50,9 +50,6 @@ import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal"; import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service"; -import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; -import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; -import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal"; import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; @@ -79,8 +76,6 @@ import { permissionServiceFactory } from "@app/ee/services/permission/permission import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; -import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; -import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal"; import { rateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; import { instanceRelayConfigDalFactory } from "@app/ee/services/relay/instance-relay-config-dal"; @@ -147,6 +142,8 @@ import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; import { registerSecretScanningV2Webhooks } from "@app/server/plugins/secret-scanner-v2"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; +import { additionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; +import { additionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -174,6 +171,7 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; +import { convertorServiceFactory } from "@app/services/convertor/convertor-service"; import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue"; @@ -187,7 +185,6 @@ import { folderCommitChangesDALFactory } from "@app/services/folder-commit-chang import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal"; import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; -import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { identityDALFactory } from "@app/services/identity/identity-dal"; import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; @@ -214,7 +211,6 @@ import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/i import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal"; import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service"; @@ -231,6 +227,14 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { membershipDALFactory } from "@app/services/membership/membership-dal"; +import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { membershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; +import { membershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service"; +import { membershipIdentityDALFactory } from "@app/services/membership-identity/membership-identity-dal"; +import { membershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service"; +import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; +import { membershipUserServiceFactory } from "@app/services/membership-user/membership-user-service"; import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal"; import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; @@ -242,8 +246,6 @@ import { offlineUsageReportServiceFactory } from "@app/services/offline-usage-re import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal"; import { orgBotDALFactory } from "@app/services/org/org-bot-dal"; import { orgDALFactory } from "@app/services/org/org-dal"; -import { orgRoleDALFactory } from "@app/services/org/org-role-dal"; -import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -274,15 +276,14 @@ import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal" import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; -import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; -import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; -import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { reminderDALFactory } from "@app/services/reminder/reminder-dal"; import { dailyReminderQueueServiceFactory } from "@app/services/reminder/reminder-queue"; import { reminderServiceFactory } from "@app/services/reminder/reminder-service"; import { reminderRecipientDALFactory } from "@app/services/reminder-recipients/reminder-recipient-dal"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { roleDALFactory } from "@app/services/role/role-dal"; +import { roleServiceFactory } from "@app/services/role/role-service"; import { secretDALFactory } from "@app/services/secret/secret-dal"; import { secretQueueFactory } from "@app/services/secret/secret-queue"; import { secretServiceFactory } from "@app/services/secret/secret-service"; @@ -382,16 +383,12 @@ export const registerRoutes = async ( const orgMembershipDAL = orgMembershipDALFactory(db); const orgBotDAL = orgBotDALFactory(db); const incidentContactDAL = incidentContactDALFactory(db); - const orgRoleDAL = orgRoleDALFactory(db); const rateLimitDAL = rateLimitDALFactory(db); const apiKeyDAL = apiKeyDALFactory(db); const projectDAL = projectDALFactory(db); const projectSshConfigDAL = projectSshConfigDALFactory(db); const projectMembershipDAL = projectMembershipDALFactory(db); - const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db); - const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db); - const projectRoleDAL = projectRoleDALFactory(db); const projectEnvDAL = projectEnvDALFactory(db); const projectKeyDAL = projectKeyDALFactory(db); const projectBotDAL = projectBotDALFactory(db); @@ -423,8 +420,6 @@ export const registerRoutes = async ( const identityAccessTokenDAL = identityAccessTokenDALFactory(db); const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db); - const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); - const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db); const identityAuthTemplateDAL = identityAuthTemplateDALFactory(db); const identityTokenAuthDAL = identityTokenAuthDALFactory(db); @@ -482,7 +477,6 @@ export const registerRoutes = async ( const gitAppOrgDAL = gitAppDALFactory(db); const groupDAL = groupDALFactory(db); const groupProjectDAL = groupProjectDALFactory(db); - const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db); const userGroupMembershipDAL = userGroupMembershipDALFactory(db); const secretScanningDAL = secretScanningDALFactory(db); const secretSharingDAL = secretSharingDALFactory(db); @@ -531,17 +525,27 @@ export const registerRoutes = async ( const secretScanningV2DAL = secretScanningV2DALFactory(db); const keyValueStoreDAL = keyValueStoreDALFactory(db); + const membershipDAL = membershipDALFactory(db); + const membershipUserDAL = membershipUserDALFactory(db); + const membershipIdentityDAL = membershipIdentityDALFactory(db); + const membershipGroupDAL = membershipGroupDALFactory(db); + const additionalPrivilegeDAL = additionalPrivilegeDALFactory(db); + const membershipRoleDAL = membershipRoleDALFactory(db); + const roleDAL = roleDALFactory(db); + const eventBusService = eventBusFactory(server.redis); const sseService = sseServiceFactory(eventBusService, server.redis); const permissionService = permissionServiceFactory({ permissionDAL, - orgRoleDAL, - projectRoleDAL, serviceTokenDAL, projectDAL, - keyStore + keyStore, + roleDAL, + userDAL, + identityDAL }); + const assumePrivilegeService = assumePrivilegeServiceFactory({ projectDAL, permissionService @@ -556,6 +560,57 @@ export const registerRoutes = async ( projectDAL }); + const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL }); + + const membershipUserService = membershipUserServiceFactory({ + licenseService, + membershipRoleDAL, + membershipUserDAL, + orgDAL, + permissionService, + roleDAL, + userDAL, + projectDAL, + projectKeyDAL, + smtpService, + tokenService, + userAliasDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL + }); + + const membershipIdentityService = membershipIdentityServiceFactory({ + membershipIdentityDAL, + membershipRoleDAL, + orgDAL, + permissionService, + roleDAL, + additionalPrivilegeDAL + }); + + const membershipGroupService = membershipGroupServiceFactory({ + membershipGroupDAL, + membershipRoleDAL, + roleDAL, + permissionService, + orgDAL + }); + + const roleService = roleServiceFactory({ + permissionService, + roleDAL, + projectDAL, + identityDAL, + userDAL, + externalGroupOrgRoleMappingDAL + }); + const additionalPrivilegeService = additionalPrivilegeServiceFactory({ + additionalPrivilegeDAL, + membershipDAL, + orgDAL, + permissionService + }); + const hsmService = hsmServiceFactory({ hsmModule, envConfig @@ -621,31 +676,29 @@ export const registerRoutes = async ( userDAL, secretApprovalRequestDAL }); - const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const samlService = samlConfigServiceFactory({ identityMetadataDAL, permissionService, orgDAL, - orgMembershipDAL, userDAL, userAliasDAL, samlConfigDAL, groupDAL, userGroupMembershipDAL, - groupProjectDAL, projectDAL, projectBotDAL, projectKeyDAL, licenseService, tokenService, smtpService, - kmsService + kmsService, + membershipRoleDAL, + membershipGroupDAL }); const groupService = groupServiceFactory({ userDAL, groupDAL, - groupProjectDAL, orgDAL, userGroupMembershipDAL, projectDAL, @@ -653,17 +706,13 @@ export const registerRoutes = async ( projectKeyDAL, permissionService, licenseService, - oidcConfigDAL + oidcConfigDAL, + membershipGroupDAL, + membershipRoleDAL }); const groupProjectService = groupProjectServiceFactory({ groupDAL, - groupProjectDAL, - groupProjectMembershipRoleDAL, - userGroupMembershipDAL, projectDAL, - projectKeyDAL, - projectBotDAL, - projectRoleDAL, permissionService }); @@ -708,18 +757,18 @@ export const registerRoutes = async ( userDAL, userAliasDAL, orgDAL, - orgMembershipDAL, projectDAL, - projectUserAdditionalPrivilegeDAL, - projectMembershipDAL, - groupDAL, - groupProjectDAL, userGroupMembershipDAL, projectKeyDAL, projectBotDAL, permissionService, smtpService, - externalGroupOrgRoleMappingDAL + externalGroupOrgRoleMappingDAL, + groupDAL, + membershipGroupDAL, + membershipRoleDAL, + membershipUserDAL, + additionalPrivilegeDAL }); const githubOrgSyncConfigService = githubOrgSyncServiceFactory({ @@ -729,16 +778,16 @@ export const registerRoutes = async ( permissionService, groupDAL, userGroupMembershipDAL, - orgMembershipDAL + orgMembershipDAL, + membershipRoleDAL, + membershipGroupDAL }); const ldapService = ldapConfigServiceFactory({ ldapConfigDAL, ldapGroupMapDAL, orgDAL, - orgMembershipDAL, groupDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -749,7 +798,9 @@ export const registerRoutes = async ( licenseService, tokenService, smtpService, - kmsService + kmsService, + membershipGroupDAL, + membershipRoleDAL }); const telemetryService = telemetryServiceFactory({ @@ -771,13 +822,12 @@ export const registerRoutes = async ( const userService = userServiceFactory({ userDAL, orgDAL, - orgMembershipDAL, tokenService, permissionService, groupProjectDAL, smtpService, - projectMembershipDAL, - userAliasDAL + userAliasDAL, + membershipUserDAL }); const upgradePathService = upgradePathServiceFactory({ keyStore }); @@ -794,17 +844,18 @@ export const registerRoutes = async ( tokenService, orgDAL, totpService, - orgMembershipDAL, auditLogService, - notificationService + notificationService, + membershipRoleDAL, + membershipUserDAL }); const passwordService = authPaswordServiceFactory({ tokenService, smtpService, authDAL, userDAL, - orgMembershipDAL, - totpConfigDAL + totpConfigDAL, + membershipUserDAL }); const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL }); @@ -826,14 +877,10 @@ export const registerRoutes = async ( folderDAL, licenseService, samlConfigDAL, - orgRoleDAL, permissionService, orgDAL, incidentContactDAL, tokenService, - projectUserAdditionalPrivilegeDAL, - projectUserMembershipRoleDAL, - projectRoleDAL, projectDAL, projectMembershipDAL, orgMembershipDAL, @@ -846,7 +893,12 @@ export const registerRoutes = async ( ldapConfigDAL, loginService, projectBotService, - reminderService + reminderService, + membershipRoleDAL, + membershipUserDAL, + roleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); const signupService = authSignupServiceFactory({ tokenService, @@ -857,18 +909,10 @@ export const registerRoutes = async ( projectKeyDAL, projectDAL, projectBotDAL, - groupProjectDAL, - projectMembershipDAL, - projectUserMembershipRoleDAL, orgDAL, orgService, - licenseService - }); - const orgRoleService = orgRoleServiceFactory({ - permissionService, - orgRoleDAL, - orgDAL, - externalGroupOrgRoleMappingDAL + licenseService, + membershipGroupDAL }); const microsoftTeamsService = microsoftTeamsServiceFactory({ @@ -885,8 +929,6 @@ export const registerRoutes = async ( userAliasDAL, identityTokenAuthDAL, identityAccessTokenDAL, - orgMembershipDAL, - identityOrgMembershipDAL, authService: loginService, serverCfgDAL: superAdminDAL, kmsRootConfigDAL, @@ -898,7 +940,10 @@ export const registerRoutes = async ( microsoftTeamsService, invalidateCacheQueue, smtpService, - tokenService + tokenService, + membershipIdentityDAL, + membershipRoleDAL, + membershipUserDAL }); const offlineUsageReportService = offlineUsageReportServiceFactory({ @@ -910,9 +955,10 @@ export const registerRoutes = async ( smtpService, projectDAL, permissionService, - projectUserMembershipRoleDAL, - projectMembershipDAL, - notificationService + notificationService, + membershipRoleDAL, + membershipUserDAL, + projectMembershipDAL }); const rateLimitService = rateLimitServiceFactory({ @@ -938,32 +984,25 @@ export const registerRoutes = async ( const projectMembershipService = projectMembershipServiceFactory({ projectMembershipDAL, - projectUserMembershipRoleDAL, projectDAL, permissionService, - projectBotDAL, - orgDAL, userDAL, - projectUserAdditionalPrivilegeDAL, userGroupMembershipDAL, smtpService, projectKeyDAL, - projectRoleDAL, groupProjectDAL, secretReminderRecipientsDAL, licenseService, - notificationService - }); - const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({ - permissionService, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, - accessApprovalRequestDAL + notificationService, + membershipUserDAL, + additionalPrivilegeDAL, + membershipRoleDAL }); + const projectKeyService = projectKeyServiceFactory({ permissionService, projectKeyDAL, - projectMembershipDAL + membershipUserDAL }); const projectQueueService = projectQueueFactory({ @@ -979,10 +1018,10 @@ export const registerRoutes = async ( secretVersionDAL, projectKeyDAL, projectBotDAL, - projectMembershipDAL, secretApprovalRequestDAL, secretApprovalSecretDAL: secretApprovalRequestSecretDAL, - projectUserMembershipRoleDAL + membershipRoleDAL, + membershipUserDAL }); const certificateAuthorityDAL = certificateAuthorityDALFactory(db); @@ -1121,7 +1160,11 @@ export const registerRoutes = async ( relayDAL, kmsService, licenseService, - permissionService + permissionService, + orgDAL, + notificationService, + smtpService, + userDAL }); const gatewayV2Service = gatewayV2ServiceFactory({ @@ -1131,7 +1174,10 @@ export const registerRoutes = async ( orgGatewayConfigV2DAL, gatewayV2DAL, relayDAL, - permissionService + permissionService, + orgDAL, + notificationService, + smtpService }); const secretSyncQueue = secretSyncQueueFactory({ @@ -1194,14 +1240,15 @@ export const registerRoutes = async ( snapshotSecretV2BridgeDAL, secretApprovalRequestDAL, projectKeyDAL, - projectUserMembershipRoleDAL, orgService, resourceMetadataDAL, folderCommitService, secretSyncQueue, reminderService, eventBusService, - licenseService + licenseService, + membershipRoleDAL, + membershipUserDAL }); const projectService = projectServiceFactory({ @@ -1212,13 +1259,10 @@ export const registerRoutes = async ( secretV2BridgeDAL, projectQueue: projectQueueService, projectBotService, - identityProjectDAL, - identityOrgMembershipDAL, userDAL, projectEnvDAL, orgDAL, projectMembershipDAL, - projectRoleDAL, folderDAL, licenseService, pkiSubscriberDAL, @@ -1232,8 +1276,6 @@ export const registerRoutes = async ( sshCertificateTemplateDAL, sshHostDAL, sshHostGroupDAL, - projectUserMembershipRoleDAL, - identityProjectMembershipRoleDAL, keyStore, kmsService, certificateTemplateDAL, @@ -1242,10 +1284,14 @@ export const registerRoutes = async ( projectMicrosoftTeamsConfigDAL, microsoftTeamsIntegrationDAL, projectTemplateService, - groupProjectDAL, smtpService, reminderService, - notificationService + notificationService, + membershipGroupDAL, + membershipIdentityDAL, + membershipRoleDAL, + membershipUserDAL, + roleDAL }); const projectEnvService = projectEnvServiceFactory({ @@ -1259,16 +1305,6 @@ export const registerRoutes = async ( secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL }); - const projectRoleService = projectRoleServiceFactory({ - permissionService, - projectRoleDAL, - projectUserMembershipRoleDAL, - identityProjectMembershipRoleDAL, - projectDAL, - identityDAL, - userDAL - }); - const snapshotService = secretSnapshotServiceFactory({ permissionService, licenseService, @@ -1423,21 +1459,18 @@ export const registerRoutes = async ( groupDAL, permissionService, projectEnvDAL, - projectMembershipDAL, projectDAL, userDAL, accessApprovalRequestDAL, - additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, accessApprovalRequestReviewerDAL, - orgMembershipDAL + additionalPrivilegeDAL, + membershipUserDAL }); const accessApprovalRequestService = accessApprovalRequestServiceFactory({ projectDAL, permissionService, accessApprovalRequestReviewerDAL, - additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, - projectMembershipDAL, accessApprovalPolicyDAL, accessApprovalRequestDAL, projectEnvDAL, @@ -1449,7 +1482,8 @@ export const registerRoutes = async ( groupDAL, microsoftTeamsService, projectMicrosoftTeamsConfigDAL, - notificationService + notificationService, + additionalPrivilegeDAL }); const secretReplicationService = secretReplicationServiceFactory({ @@ -1538,7 +1572,15 @@ export const registerRoutes = async ( identityProjectDAL, licenseService, identityMetadataDAL, - keyStore + keyStore, + orgDAL, + membershipIdentityDAL, + membershipRoleDAL + }); + const identityProjectService = identityProjectServiceFactory({ + identityProjectDAL, + membershipIdentityDAL, + permissionService }); const identityAuthTemplateService = identityAuthTemplateServiceFactory({ @@ -1557,105 +1599,91 @@ export const registerRoutes = async ( identityDAL }); - const identityProjectService = identityProjectServiceFactory({ - permissionService, - projectDAL, - identityProjectDAL, - identityOrgMembershipDAL, - identityProjectMembershipRoleDAL, - projectRoleDAL - }); - const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({ - projectDAL, - identityProjectAdditionalPrivilegeDAL, - permissionService, - identityProjectDAL - }); - - const identityProjectAdditionalPrivilegeV2Service = identityProjectAdditionalPrivilegeV2ServiceFactory({ - projectDAL, - identityProjectAdditionalPrivilegeDAL, - permissionService, - identityProjectDAL - }); - const identityTokenAuthService = identityTokenAuthServiceFactory({ identityTokenAuthDAL, - identityOrgMembershipDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + orgDAL, + membershipIdentityDAL }); const identityUaService = identityUaServiceFactory({ - identityOrgMembershipDAL, permissionService, identityAccessTokenDAL, identityUaClientSecretDAL, identityUaDAL, licenseService, - keyStore + keyStore, + orgDAL, + membershipIdentityDAL }); const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ identityKubernetesAuthDAL, - identityOrgMembershipDAL, identityAccessTokenDAL, permissionService, licenseService, gatewayService, + orgDAL, gatewayV2Service, gatewayV2DAL, gatewayDAL, - kmsService + kmsService, + membershipIdentityDAL }); const identityGcpAuthService = identityGcpAuthServiceFactory({ identityGcpAuthDAL, - identityOrgMembershipDAL, + orgDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + membershipIdentityDAL }); const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({ identityAccessTokenDAL, + orgDAL, identityAliCloudAuthDAL, - identityOrgMembershipDAL, licenseService, - permissionService + permissionService, + membershipIdentityDAL }); const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({ identityAccessTokenDAL, identityTlsCertAuthDAL, - identityOrgMembershipDAL, licenseService, permissionService, - kmsService + kmsService, + membershipIdentityDAL }); const identityAwsAuthService = identityAwsAuthServiceFactory({ identityAccessTokenDAL, + orgDAL, identityAwsAuthDAL, - identityOrgMembershipDAL, licenseService, - permissionService + permissionService, + membershipIdentityDAL }); const identityAzureAuthService = identityAzureAuthServiceFactory({ identityAzureAuthDAL, - identityOrgMembershipDAL, + orgDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + membershipIdentityDAL }); const identityOciAuthService = identityOciAuthServiceFactory({ identityAccessTokenDAL, + orgDAL, identityOciAuthDAL, - identityOrgMembershipDAL, licenseService, - permissionService + permissionService, + membershipIdentityDAL }); const pitService = pitServiceFactory({ @@ -1674,32 +1702,42 @@ export const registerRoutes = async ( const identityOidcAuthService = identityOidcAuthServiceFactory({ identityOidcAuthDAL, - identityOrgMembershipDAL, + orgDAL, identityAccessTokenDAL, permissionService, licenseService, - kmsService + kmsService, + membershipIdentityDAL }); const identityJwtAuthService = identityJwtAuthServiceFactory({ identityJwtAuthDAL, + orgDAL, permissionService, identityAccessTokenDAL, - identityOrgMembershipDAL, licenseService, - kmsService + kmsService, + membershipIdentityDAL }); const identityLdapAuthService = identityLdapAuthServiceFactory({ identityLdapAuthDAL, + orgDAL, permissionService, kmsService, identityAccessTokenDAL, - identityOrgMembershipDAL, licenseService, identityDAL, identityAuthTemplateDAL, - keyStore + keyStore, + membershipIdentityDAL + }); + + const convertorService = convertorServiceFactory({ + additionalPrivilegeDAL, + membershipDAL, + projectDAL, + groupDAL }); const dynamicSecretProviders = buildDynamicSecretProviders({ @@ -1774,7 +1812,6 @@ export const registerRoutes = async ( const oidcService = oidcConfigServiceFactory({ orgDAL, - orgMembershipDAL, userDAL, userAliasDAL, licenseService, @@ -1787,9 +1824,10 @@ export const registerRoutes = async ( projectKeyDAL, projectDAL, userGroupMembershipDAL, - groupProjectDAL, groupDAL, - auditLogService + auditLogService, + membershipGroupDAL, + membershipRoleDAL }); const userEngagementService = userEngagementServiceFactory({ @@ -1845,8 +1883,8 @@ export const registerRoutes = async ( const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({ permissionService, licenseService, - orgRoleDAL, - externalGroupOrgRoleMappingDAL + externalGroupOrgRoleMappingDAL, + roleDAL }); const appConnectionService = appConnectionServiceFactory({ @@ -2192,7 +2230,6 @@ export const registerRoutes = async ( groupProject: groupProjectService, permission: permissionService, org: orgService, - orgRole: orgRoleService, oidc: oidcService, apiKey: apiKeyService, authToken: tokenService, @@ -2202,7 +2239,6 @@ export const registerRoutes = async ( projectMembership: projectMembershipService, projectKey: projectKeyService, projectEnv: projectEnvService, - projectRole: projectRoleService, secret: secretService, secretReplication: secretReplicationService, secretTag: secretTagService, @@ -2217,7 +2253,6 @@ export const registerRoutes = async ( identity: identityService, identityAuthTemplate: identityAuthTemplateService, identityAccessToken: identityAccessTokenService, - identityProject: identityProjectService, identityTokenAuth: identityTokenAuthService, identityUa: identityUaService, identityKubernetesAuth: identityKubernetesAuthService, @@ -2264,9 +2299,6 @@ export const registerRoutes = async ( scim: scimService, secretBlindIndex: secretBlindIndexService, telemetry: telemetryService, - projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, - identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, - identityProjectAdditionalPrivilegeV2: identityProjectAdditionalPrivilegeV2Service, secretSharing: secretSharingService, userEngagement: userEngagementService, externalKms: externalKmsService, @@ -2300,7 +2332,15 @@ export const registerRoutes = async ( pamResource: pamResourceService, pamAccount: pamAccountService, pamSession: pamSessionService, - upgradePath: upgradePathService + upgradePath: upgradePathService, + + membershipUser: membershipUserService, + membershipIdentity: membershipIdentityService, + membershipGroup: membershipGroupService, + role: roleService, + additionalPrivilege: additionalPrivilegeService, + identityProject: identityProjectService, + convertor: convertorService }); const cronJobs: CronJob[] = []; @@ -2330,6 +2370,16 @@ export const registerRoutes = async ( cronJobs.push(configSyncJob); } + const gatewayHealthcheckNotifyJob = await gatewayV2Service.initializeHealthcheckNotify(); + if (gatewayHealthcheckNotifyJob) { + cronJobs.push(gatewayHealthcheckNotifyJob); + } + + const relayHealthcheckNotifyJob = await relayService.initializeHealthcheckNotify(); + if (relayHealthcheckNotifyJob) { + cronJobs.push(relayHealthcheckNotifyJob); + } + const oauthConfigSyncJob = await initializeOauthConfigSync(); if (oauthConfigSyncJob) { cronJobs.push(oauthConfigSyncJob); diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 3442240080..47fbb0e07f 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -209,11 +209,11 @@ export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivile ) }); -export const SanitizedRoleSchema = ProjectRolesSchema.extend({ +export const SanitizedRoleSchema = ProjectRolesSchema.omit({ version: true }).extend({ permissions: UnpackedPermissionSchema.array() }); -export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({ +export const SanitizedRoleSchemaV1 = ProjectRolesSchema.omit({ version: true }).extend({ permissions: UnpackedPermissionSchema.array().transform((caslPermission) => // first map and remove other actions of folder permission caslPermission diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 7f5a2f3745..ddb3f23264 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -5,6 +5,7 @@ import { IdentitiesSchema, OrganizationsSchema, OrgMembershipsSchema, + OrgMembershipStatus, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; @@ -172,7 +173,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { email: true, id: true, superAdmin: true - }).array() + }).array(), + total: z.number() }) } }, @@ -182,13 +184,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const users = await server.services.superAdmin.getUsers({ + const result = await server.services.superAdmin.getUsers({ ...req.query }); - return { - users - }; + return result; } }); @@ -230,7 +230,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { createdAt: z.date() }) .array() - }).array() + }).array(), + total: z.number() }) } }, @@ -240,13 +241,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const organizations = await server.services.superAdmin.getOrganizations({ + const result = await server.services.superAdmin.getOrganizations({ ...req.query }); - return { - organizations - }; + return result; } }); @@ -281,7 +280,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { ); return { - organizationMembership + organizationMembership: { + ...organizationMembership, + status: organizationMembership?.status || OrgMembershipStatus.Accepted + } }; } }); @@ -337,7 +339,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { .extend({ isInstanceAdmin: z.boolean() }) - .array() + .array(), + total: z.number() }) } }, @@ -347,13 +350,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const identities = await server.services.superAdmin.getIdentities({ + const result = await server.services.superAdmin.getIdentities({ ...req.query }); - return { - identities - }; + return result; } }); @@ -895,7 +896,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const organizationMembership = await server.services.superAdmin.resendOrgInvite(req.params, req.permission); - return { organizationMembership }; + + return { + organizationMembership: { + ...organizationMembership, + status: organizationMembership?.status || OrgMembershipStatus.Accepted + } + }; } }); @@ -925,7 +932,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { req.params.organizationId, req.permission ); - return { organizationMembership }; + return { + organizationMembership: { + ...organizationMembership, + status: organizationMembership?.status || OrgMembershipStatus.Accepted + } + }; } }); diff --git a/backend/src/server/routes/v1/deprecated-project-membership-router.ts b/backend/src/server/routes/v1/deprecated-project-membership-router.ts index ab225929f5..25e07a4669 100644 --- a/backend/src/server/routes/v1/deprecated-project-membership-router.ts +++ b/backend/src/server/routes/v1/deprecated-project-membership-router.ts @@ -1,9 +1,13 @@ import { z } from "zod"; import { + AccessScope, + OrgMembershipRole, OrgMembershipsSchema, + OrgMembershipStatus, ProjectMembershipsSchema, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -13,7 +17,6 @@ import { ms } from "@app/lib/ms"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ @@ -66,14 +69,23 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const memberships = await server.services.projectMembership.getProjectMemberships({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + const { data: memberships } = await server.services.membershipUser.listMemberships({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + data: {} }); - return { memberships }; + + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.workspaceId + })) + }; } }); @@ -124,15 +136,30 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.getProjectMembershipById({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - id: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + const membership = await server.services.membershipUser.getMembershipByUserId({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + selector: { + userId + } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.workspaceId + } + }; } }); @@ -241,14 +268,22 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ ...req.auditLogInfo, event: { type: EventType.ADD_BATCH_PROJECT_MEMBER, - metadata: data.map(({ userId }) => ({ - userId: userId || "", + metadata: data.map(({ actorUserId }) => ({ + userId: actorUserId || "", email: "" })) } }); - return { data, success: true }; + return { + data: data.map((el) => ({ + ...el, + orgId: req.permission.orgId, + role: OrgMembershipRole.Member, + status: el.status || OrgMembershipStatus.Accepted + })), + success: true + }; } }); @@ -282,7 +317,7 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -300,30 +335,28 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectMembership.updateProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - membershipId: req.params.membershipId, - roles: req.body.roles + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.updateMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + selector: { + userId + }, + data: { + roles: req.body.roles + } }); - // await server.services.auditLog.createAuditLog({ - // ...req.auditLogInfo, - // projectId: req.params.workspaceId, - // event: { - // type: EventType.UPDATE_USER_WORKSPACE_ROLE, - // metadata: { - // userId: membership.userId, - // newRole: req.body.role, - // oldRole: membership.role, - // email: "" - // } - // } - // }); - return { roles }; + return { roles: membership.roles.map((el) => ({ ...el, projectMembershipId: req.params.membershipId })) }; } }); @@ -352,13 +385,22 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.deleteProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - membershipId: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + selector: { + userId + } }); await server.services.auditLog.createAuditLog({ @@ -367,12 +409,19 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ event: { type: EventType.REMOVE_PROJECT_MEMBER, metadata: { - userId: membership.userId, + userId: membership.actorUserId as string, email: "" } } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.workspaceId + } + }; } }); }; diff --git a/backend/src/server/routes/v1/group-project-router.ts b/backend/src/server/routes/v1/group-project-router.ts index d07e3bd8ba..93caf50351 100644 --- a/backend/src/server/routes/v1/group-project-router.ts +++ b/backend/src/server/routes/v1/group-project-router.ts @@ -1,19 +1,21 @@ import { z } from "zod"; import { + AccessScope, GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UsersSchema } from "@app/db/schemas"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; +import { isUuidV4 } from "@app/lib/validator"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ @@ -54,7 +56,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -73,17 +75,32 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.addGroupToProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - roles: req.body.roles || [{ role: req.body.role }], - projectId: req.params.projectId, - groupIdOrName: req.params.groupIdOrName + let groupId = req.params.groupIdOrName; + if (!isUuidV4(req.params.groupIdOrName)) { + const groupDetails = await server.services.convertor.getGroupIdFromName(groupId, req.permission.orgId); + groupId = groupDetails.groupId; + } + + const { membership: groupMembership } = await server.services.membershipGroup.createMembership({ + permission: req.permission, + data: { + groupId, + roles: req.body.roles || [{ role: req.body.role, isTemporary: false }] + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -115,7 +132,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -131,17 +148,22 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const roles = await server.services.groupProject.updateGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - groupId: req.params.groupId, - roles: req.body.roles + const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + data: { + roles: req.body.roles + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { roles }; + return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) }; } }); @@ -172,16 +194,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.removeGroupFromProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - groupId: req.params.groupId, - projectId: req.params.projectId + const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -233,15 +264,17 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMemberships = await server.services.groupProject.listGroupsInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({ + permission: req.permission, + data: {}, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMemberships }; + return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) }; } }); @@ -292,15 +325,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params + const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); diff --git a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts index a9b2d9b031..4c444d7043 100644 --- a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -78,7 +78,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH, metadata: { 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 effd66a684..fe7cf7d5b9 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 @@ -45,7 +45,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_AWS_AUTH, metadata: { 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 9053bc2e35..9ef733a251 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -40,7 +40,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_AZURE_AUTH, metadata: { 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 b83faf9d92..91e8038a7e 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -40,7 +40,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_GCP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index 373a8b927e..ecffaebe14 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -119,7 +119,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_JWT_AUTH, metadata: { 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 879310790e..6f0500c294 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -64,7 +64,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index 512f253e08..ac384d2164 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -168,7 +168,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_LDAP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oci-auth-router.ts b/backend/src/server/routes/v1/identity-oci-auth-router.ts index e529f300bd..df19492a67 100644 --- a/backend/src/server/routes/v1/identity-oci-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -57,7 +57,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_OCI_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 74cd94eb5c..147105aba4 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -67,7 +67,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_OIDC_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-project-router.ts b/backend/src/server/routes/v1/identity-project-router.ts index 9de7ed1aaa..fd39c7efee 100644 --- a/backend/src/server/routes/v1/identity-project-router.ts +++ b/backend/src/server/routes/v1/identity-project-router.ts @@ -1,10 +1,12 @@ import { z } from "zod"; import { + AccessScope, IdentitiesSchema, IdentityProjectMembershipsSchema, ProjectMembershipRole, - ProjectUserMembershipRolesSchema + ProjectUserMembershipRolesSchema, + TemporaryPermissionMode } from "@app/db/schemas"; import { ApiDocsTags, ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; @@ -14,7 +16,6 @@ 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 { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types"; -import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; import { SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -56,7 +57,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), temporaryMode: z - .nativeEnum(ProjectUserMembershipTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), temporaryRange: z .string() @@ -82,16 +83,22 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) const { role, roles } = req.body; if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" }); - const identityMembership = await server.services.identityProject.createProjectIdentity({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - projectId: req.params.projectId, - roles: roles || [{ role }] + const { membership } = await server.services.membershipIdentity.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + data: { + identityId: req.params.identityId, + roles: roles || [{ role, isTemporary: false }] + } }); - return { identityMembership }; + + return { + identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId } + }; } }); @@ -130,7 +137,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role), isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary), temporaryMode: z - .nativeEnum(ProjectUserMembershipTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode), temporaryRange: z .string() @@ -153,16 +160,24 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const roles = await server.services.identityProject.updateProjectIdentity({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - projectId: req.params.projectId, - roles: req.body.roles + const { membership } = await server.services.membershipIdentity.updateMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + identityId: req.params.identityId + }, + data: { + roles: req.body.roles + } }); - return { roles }; + + return { + roles: membership.roles.map((el) => ({ ...el, projectMembershipId: membership.id })) + }; } }); @@ -193,15 +208,21 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const identityMembership = await server.services.identityProject.deleteProjectIdentity({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - projectId: req.params.projectId + const { membership } = await server.services.membershipIdentity.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + identityId: req.params.identityId + } }); - return { identityMembership }; + + return { + identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId } + }; } }); diff --git a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts index 0bb9e08ea6..0503ed16f0 100644 --- a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts +++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts @@ -72,7 +72,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index e22c41889d..9a33d0651c 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -332,7 +332,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH, metadata: { @@ -393,7 +393,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH, metadata: { @@ -447,7 +447,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index 6d911a88e1..153e1e6413 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -59,7 +59,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index b98e94be0b..0f78e04482 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, UsersSchema } from "@app/db/schemas"; import { inviteUserRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -23,13 +23,6 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { .array() .refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"), organizationId: z.string().trim(), - projects: z - .object({ - id: z.string(), - projectRoleSlug: z.string().array().default([ProjectMembershipRole.Member]) - }) - .array() - .optional(), organizationRoleSlug: z.string().default(OrgMembershipRole.Member) }), response: { @@ -50,15 +43,16 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { handler: async (req) => { if (req.auth.actor !== ActorType.USER) return; - const { signupTokens: completeInviteLinks } = await server.services.org.inviteUserToOrganization({ - orgId: req.body.organizationId, - actor: req.permission.type, - actorId: req.permission.id, - inviteeEmails: req.body.inviteeEmails, - projects: req.body.projects, - organizationRoleSlug: req.body.organizationRoleSlug, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + const { signUpTokens: completeInviteLinks } = await server.services.membershipUser.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + data: { + usernames: req.body.inviteeEmails, + roles: [{ isTemporary: false, role: req.body.organizationRoleSlug }] + } }); await server.services.telemetry.sendPostHogEvents({ diff --git a/backend/src/server/routes/v1/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts index d4b1ee1883..8260b3a8fb 100644 --- a/backend/src/server/routes/v1/org-admin-router.ts +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -86,7 +86,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { }); } - return { membership }; + return { membership: { ...membership, userId: req.permission.id, projectId: req.params.projectId } }; } }); }; diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index b8eb3ad6b1..872b7b157d 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -6,6 +6,7 @@ import { GroupsSchema, IncidentContactsSchema, OrgMembershipsSchema, + OrgMembershipStatus, OrgRolesSchema, UsersSchema } from "@app/db/schemas"; @@ -263,7 +264,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.authMethod, req.permission.orgId ); - return { users }; + return { users: users.map((el) => ({ ...el, status: el.status || OrgMembershipStatus.Accepted })) }; } }); diff --git a/backend/src/server/routes/v1/pki-sync-routers/aws-certificate-manager-pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/aws-certificate-manager-pki-sync-router.ts new file mode 100644 index 0000000000..21bfadbac3 --- /dev/null +++ b/backend/src/server/routes/v1/pki-sync-routers/aws-certificate-manager-pki-sync-router.ts @@ -0,0 +1,22 @@ +import { + AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION, + AwsCertificateManagerPkiSyncSchema, + CreateAwsCertificateManagerPkiSyncSchema, + UpdateAwsCertificateManagerPkiSyncSchema +} from "@app/services/pki-sync/aws-certificate-manager"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +import { registerSyncPkiEndpoints } from "./pki-sync-endpoints"; + +export const registerAwsCertificateManagerPkiSyncRouter = async (server: FastifyZodProvider) => + registerSyncPkiEndpoints({ + destination: PkiSync.AwsCertificateManager, + server, + responseSchema: AwsCertificateManagerPkiSyncSchema, + createSchema: CreateAwsCertificateManagerPkiSyncSchema, + updateSchema: UpdateAwsCertificateManagerPkiSyncSchema, + syncOptions: { + canImportCertificates: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION.canImportCertificates, + canRemoveCertificates: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION.canRemoveCertificates + } + }); diff --git a/backend/src/server/routes/v1/pki-sync-routers/index.ts b/backend/src/server/routes/v1/pki-sync-routers/index.ts index 326b650e94..4b81db27f2 100644 --- a/backend/src/server/routes/v1/pki-sync-routers/index.ts +++ b/backend/src/server/routes/v1/pki-sync-routers/index.ts @@ -1,9 +1,11 @@ import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; +import { registerAwsCertificateManagerPkiSyncRouter } from "./aws-certificate-manager-pki-sync-router"; import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router"; export * from "./pki-sync-router"; export const PKI_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { - [PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter + [PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter, + [PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter }; diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index f3cdfe7016..57fdad031c 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -1,10 +1,11 @@ import { z } from "zod"; import { - OrgMembershipRole, + AccessScope, ProjectMembershipRole, ProjectMembershipsSchema, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -14,7 +15,6 @@ import { ms } from "@app/lib/ms"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ @@ -67,14 +67,23 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const memberships = await server.services.projectMembership.getProjectMemberships({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { data: memberships } = await server.services.membershipUser.listMemberships({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + data: {} }); - return { memberships }; + + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.projectId + })) + }; } }); @@ -125,15 +134,30 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.getProjectMembershipById({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - id: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + const membership = await server.services.membershipUser.getMembershipByUserId({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + userId + } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.projectId + } + }; } }); @@ -242,20 +266,17 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const usernamesAndEmails = [...req.body.emails, ...req.body.usernames]; - const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - inviteeEmails: usernamesAndEmails, - orgId: req.permission.orgId, - organizationRoleSlug: OrgMembershipRole.NoAccess, - projects: [ - { - id: req.params.projectId, - projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member] - } - ] + const { memberships } = await server.services.membershipUser.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + data: { + roles: (req.body.roleSlugs || [ProjectMembershipRole.Member]).map((role) => ({ isTemporary: false, role })), + usernames: usernamesAndEmails + } }); await server.services.auditLog.createAuditLog({ @@ -263,15 +284,21 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider ...req.auditLogInfo, event: { type: EventType.ADD_BATCH_PROJECT_MEMBER, - metadata: memberships.map(({ userId, id }) => ({ - userId: userId || "", + metadata: memberships.map(({ actorUserId, id }) => ({ + userId: actorUserId || "", membershipId: id, email: "" })) } }); - return { memberships }; + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.projectId + })) + }; } }); @@ -305,7 +332,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -323,17 +350,28 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectMembership.updateProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - membershipId: req.params.membershipId, - roles: req.body.roles + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.updateMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + userId + }, + data: { + roles: req.body.roles + } }); - return { roles }; + return { roles: membership.roles.map((el) => ({ ...el, projectMembershipId: req.params.membershipId })) }; } }); @@ -396,13 +434,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider event: { type: EventType.REMOVE_PROJECT_MEMBER, metadata: { - userId: membership.userId, + userId: membership.actorUserId as string, email: "" } } }); } - return { memberships }; + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.projectId + })) + }; } }); @@ -431,13 +475,22 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.deleteProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - membershipId: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + userId + } }); await server.services.auditLog.createAuditLog({ @@ -446,12 +499,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider event: { type: EventType.REMOVE_PROJECT_MEMBER, metadata: { - userId: membership.userId, + userId: membership.actorUserId as string, email: "" } } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.projectId + } + }; } }); @@ -479,7 +539,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider actor: req.permission.type, projectId: req.params.projectId }); - return { membership }; + return { + membership: { + ...membership, + userId: membership.actorUserId as string, + projectId: req.params.projectId + } + }; } }); }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts index 9db3a2013c..9da2f4b693 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts @@ -425,4 +425,42 @@ export const registerSyncSecretsEndpoints = { + const { destinationConfig, excludeSyncId, projectId } = req.body; + + const result = await server.services.secretSync.checkDuplicateDestination( + { + destinationConfig: destinationConfig as Record, + destination, + excludeSyncId, + projectId + }, + req.permission + ); + + return result; + } + }); }; diff --git a/backend/src/server/routes/v2/deprecated-group-project-router.ts b/backend/src/server/routes/v2/deprecated-group-project-router.ts index f0e4ee705a..5be7df8387 100644 --- a/backend/src/server/routes/v2/deprecated-group-project-router.ts +++ b/backend/src/server/routes/v2/deprecated-group-project-router.ts @@ -1,19 +1,21 @@ import { z } from "zod"; import { + AccessScope, GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UsersSchema } from "@app/db/schemas"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; +import { isUuidV4 } from "@app/lib/validator"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ @@ -54,7 +56,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -73,17 +75,32 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.addGroupToProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - roles: req.body.roles || [{ role: req.body.role }], - projectId: req.params.projectId, - groupIdOrName: req.params.groupIdOrName + let groupId = req.params.groupIdOrName; + if (!isUuidV4(req.params.groupIdOrName)) { + const groupDetails = await server.services.convertor.getGroupIdFromName(groupId, req.permission.orgId); + groupId = groupDetails.groupId; + } + + const { membership: groupMembership } = await server.services.membershipGroup.createMembership({ + permission: req.permission, + data: { + groupId, + roles: req.body.roles || [{ role: req.body.role, isTemporary: false }] + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -115,7 +132,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -131,17 +148,22 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const roles = await server.services.groupProject.updateGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - groupId: req.params.groupId, - roles: req.body.roles + const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + data: { + roles: req.body.roles + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { roles }; + return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) }; } }); @@ -172,16 +194,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.removeGroupFromProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - groupId: req.params.groupId, - projectId: req.params.projectId + const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -233,15 +264,17 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMemberships = await server.services.groupProject.listGroupsInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({ + permission: req.permission, + data: {}, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMemberships }; + return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) }; } }); @@ -292,15 +325,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params + const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); diff --git a/backend/src/server/routes/v2/deprecated-identity-project-router.ts b/backend/src/server/routes/v2/deprecated-identity-project-router.ts index c16c874aec..85ea7d1124 100644 --- a/backend/src/server/routes/v2/deprecated-identity-project-router.ts +++ b/backend/src/server/routes/v2/deprecated-identity-project-router.ts @@ -1,10 +1,12 @@ import { z } from "zod"; import { + AccessScope, IdentitiesSchema, IdentityProjectMembershipsSchema, ProjectMembershipRole, - ProjectUserMembershipRolesSchema + ProjectUserMembershipRolesSchema, + TemporaryPermissionMode } from "@app/db/schemas"; import { ApiDocsTags, ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; @@ -14,7 +16,6 @@ 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 { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types"; -import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; import { SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -56,7 +57,7 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), temporaryMode: z - .nativeEnum(ProjectUserMembershipTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), temporaryRange: z .string() @@ -82,16 +83,22 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod const { role, roles } = req.body; if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" }); - const identityMembership = await server.services.identityProject.createProjectIdentity({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - projectId: req.params.projectId, - roles: roles || [{ role }] + const { membership } = await server.services.membershipIdentity.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + data: { + identityId: req.params.identityId, + roles: roles || [{ role, isTemporary: false }] + } }); - return { identityMembership }; + + return { + identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId } + }; } }); @@ -130,7 +137,7 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role), isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary), temporaryMode: z - .nativeEnum(ProjectUserMembershipTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode), temporaryRange: z .string() @@ -153,16 +160,24 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod } }, handler: async (req) => { - const roles = await server.services.identityProject.updateProjectIdentity({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - projectId: req.params.projectId, - roles: req.body.roles + const { membership } = await server.services.membershipIdentity.updateMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + identityId: req.params.identityId + }, + data: { + roles: req.body.roles + } }); - return { roles }; + + return { + roles: membership.roles.map((el) => ({ ...el, projectMembershipId: membership.id })) + }; } }); @@ -193,15 +208,21 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod } }, handler: async (req) => { - const identityMembership = await server.services.identityProject.deleteProjectIdentity({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - identityId: req.params.identityId, - projectId: req.params.projectId + const { membership } = await server.services.membershipIdentity.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + identityId: req.params.identityId + } }); - return { identityMembership }; + + return { + identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId } + }; } }); diff --git a/backend/src/server/routes/v2/deprecated-project-membership-router.ts b/backend/src/server/routes/v2/deprecated-project-membership-router.ts index d88d2f996a..4dff4d5ea9 100644 --- a/backend/src/server/routes/v2/deprecated-project-membership-router.ts +++ b/backend/src/server/routes/v2/deprecated-project-membership-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs"; import { writeLimit } from "@app/server/config/rateLimiter"; @@ -51,20 +51,17 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const usernamesAndEmails = [...req.body.emails, ...req.body.usernames]; - const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - inviteeEmails: usernamesAndEmails, - orgId: req.permission.orgId, - organizationRoleSlug: OrgMembershipRole.NoAccess, - projects: [ - { - id: req.params.projectId, - projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member] - } - ] + const { memberships } = await server.services.membershipUser.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + data: { + usernames: usernamesAndEmails, + roles: (req.body.roleSlugs || [ProjectMembershipRole.Member]).map((role) => ({ isTemporary: false, role })) + } }); await server.services.auditLog.createAuditLog({ @@ -72,15 +69,21 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ ...req.auditLogInfo, event: { type: EventType.ADD_BATCH_PROJECT_MEMBER, - metadata: memberships.map(({ userId, id }) => ({ - userId: userId || "", + metadata: memberships.map(({ actorUserId, id }) => ({ + userId: actorUserId || "", membershipId: id, email: "" })) } }); - return { memberships }; + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.projectId + })) + }; } }); @@ -143,13 +146,19 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ event: { type: EventType.REMOVE_PROJECT_MEMBER, metadata: { - userId: membership.userId, + userId: membership.actorUserId as string, email: "" } } }); } - return { memberships }; + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.projectId + })) + }; } }); }; diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 9135d2356b..93320f3e0d 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { OrgMembershipsSchema, + OrgMembershipStatus, ProjectMembershipsSchema, ProjectsSchema, UserEncryptionKeysSchema, @@ -63,7 +64,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.authMethod, req.permission.orgId ); - return { users }; + return { users: users.map((el) => ({ ...el, status: el.status || OrgMembershipStatus.Accepted })) }; } }); @@ -138,6 +139,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ membership: OrgMembershipsSchema.extend({ + customRoleSlug: z.string().nullish(), metadata: z .object({ key: z.string().trim().min(1), @@ -168,7 +170,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { orgId: req.params.organizationId, membershipId: req.params.membershipId }); - return { membership }; + return { membership: { ...membership, status: membership.status || OrgMembershipStatus.Accepted } }; } }); @@ -220,7 +222,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, ...req.body }); - return { membership }; + return { + membership: { + ...membership, + role: req.body.role || "", + orgId: req.params.organizationId, + status: membership.status || OrgMembershipStatus.Accepted + } + }; } }); @@ -260,7 +269,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { membershipId: req.params.membershipId, actorOrgId: req.permission.orgId }); - return { membership }; + return { + membership: { + ...membership, + status: membership.status || OrgMembershipStatus.Accepted, + role: "", + orgId: req.params.organizationId + } + }; } }); @@ -302,7 +318,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { membershipIds: req.body.membershipIds, actorOrgId: req.permission.orgId }); - return { memberships }; + return { + memberships: memberships.map((el) => ({ + ...el, + status: el?.status || OrgMembershipStatus.Accepted, + role: "", + orgId: req.params.organizationId + })) + }; } }); diff --git a/backend/src/services/additional-privilege/additional-privilege-dal.ts b/backend/src/services/additional-privilege/additional-privilege-dal.ts new file mode 100644 index 0000000000..b11bd90d68 --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-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 TAdditionalPrivilegeDALFactory = ReturnType; + +export const additionalPrivilegeDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.AdditionalPrivilege); + + return orm; +}; diff --git a/backend/src/services/additional-privilege/additional-privilege-service.ts b/backend/src/services/additional-privilege/additional-privilege-service.ts new file mode 100644 index 0000000000..2af9e6419a --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-service.ts @@ -0,0 +1,254 @@ +// eslint-disable-next-line simple-import-sort/imports +import { RawRule } from "@casl/ability"; +import { packRules } from "@casl/ability/extra"; + +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; + +import { TMembershipDALFactory } from "../membership/membership-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TAdditionalPrivilegeDALFactory } from "./additional-privilege-dal"; +import { + TAdditionalPrivilegesScopeFactory, + TCreateAdditionalPrivilegesDTO, + TDeleteAdditionalPrivilegesDTO, + TGetAdditionalPrivilegesByIdDTO, + TGetAdditionalPrivilegesByNameDTO, + TListAdditionalPrivilegesDTO, + TUpdateAdditionalPrivilegesDTO +} from "./additional-privilege-types"; +import { newNamespaceAdditionalPrivilegesFactory } from "./namespace/namespace-additional-privilege-factory"; +import { newOrgAdditionalPrivilegesFactory } from "./org/org-additional-privilege-factory"; +import { newProjectAdditionalPrivilegesFactory } from "./project/project-additional-privilege-factory"; +import { ActorType } from "../auth/auth-type"; + +type TAdditionalPrivilegeServiceFactoryDep = { + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; + permissionService: Pick; + orgDAL: Pick; + membershipDAL: Pick; +}; + +export type TAdditionalPrivilegeServiceFactory = ReturnType; + +export const additionalPrivilegeServiceFactory = ({ + additionalPrivilegeDAL, + permissionService, + orgDAL, + membershipDAL +}: TAdditionalPrivilegeServiceFactoryDep) => { + const scopeFactory: Record = { + [AccessScope.Organization]: newOrgAdditionalPrivilegesFactory({}), + [AccessScope.Project]: newProjectAdditionalPrivilegesFactory({ + membershipDAL, + orgDAL, + permissionService + }), + [AccessScope.Namespace]: newNamespaceAdditionalPrivilegesFactory({}) + }; + + const createAdditionalPrivilege = async (dto: TCreateAdditionalPrivilegesDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onCreateAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = data.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const existingSlug = await additionalPrivilegeDAL.findOne({ + name: data.name, + [dbActorField]: data.actorId, + [scope.key]: scope.value + }); + if (existingSlug) throw new BadRequestError({ message: `Additional privilege with name ${data.name} exists` }); + + validateHandlebarTemplate("Additional Privilege Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + if (!data.isTemporary) { + const additionalPrivilege = await additionalPrivilegeDAL.create({ + name: data.name, + [dbActorField]: data.actorId, + [scope.key]: scope.value, + isTemporary: data.isTemporary, + permissions: JSON.stringify(packRules(data.permissions as RawRule[])) + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + } + + if (!data.temporaryAccessStartTime || !data.temporaryRange) { + throw new BadRequestError({ message: "Temporary mode expects start time and range" }); + } + + const relativeTempAllocatedTimeInMs = ms(data.temporaryRange); + const additionalPrivilege = await additionalPrivilegeDAL.create({ + [dbActorField]: data.actorId, + [scope.key]: scope.value, + name: data.name, + isTemporary: data.isTemporary, + permissions: JSON.stringify(packRules(data.permissions as RawRule[])), + temporaryAccessEndTime: new Date( + new Date(data.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs + ), + temporaryAccessStartTime: new Date(data.temporaryAccessStartTime), + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: data.temporaryRange + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const updateAdditionalPrivilege = async (dto: TUpdateAdditionalPrivilegesDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onUpdateAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const existingPrivilege = await additionalPrivilegeDAL.findOne({ + [dbActorField]: dto.selector.actorId, + id: dto.selector.id, + [scope.key]: scope.value + }); + if (!existingPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${dto.selector.id} doesn't exist` }); + + validateHandlebarTemplate("Additional Privilege Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + const updatedData = { ...existingPrivilege, ...data }; + + if (!updatedData.isTemporary) { + const additionalPrivilege = await additionalPrivilegeDAL.updateById(existingPrivilege.id, { + name: updatedData.name, + isTemporary: data.isTemporary, + permissions: data.permissions ? JSON.stringify(packRules(data.permissions as RawRule[])) : undefined + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + } + + if (!updatedData.temporaryAccessStartTime || !updatedData.temporaryRange) { + throw new BadRequestError({ message: "Temporary mode expects start time and range" }); + } + + const relativeTempAllocatedTimeInMs = ms(updatedData.temporaryRange); + const additionalPrivilege = await additionalPrivilegeDAL.updateById(existingPrivilege.id, { + name: updatedData.name, + isTemporary: updatedData.isTemporary, + permissions: JSON.stringify(packRules(updatedData.permissions as RawRule[])), + temporaryAccessEndTime: new Date( + new Date(updatedData.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs + ), + temporaryAccessStartTime: new Date(updatedData.temporaryAccessStartTime), + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: updatedData.temporaryRange + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const deleteAdditionalPrivilege = async (dto: TDeleteAdditionalPrivilegesDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onDeleteAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const existingPrivilege = await additionalPrivilegeDAL.findOne({ + id: selector.id, + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + if (!existingPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); + + const additionalPrivilege = await additionalPrivilegeDAL.deleteById(existingPrivilege.id); + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const getAdditionalPrivilegeById = async (dto: TGetAdditionalPrivilegesByIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onGetAdditionalPrivilegesByIdGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const additionalPrivilege = await additionalPrivilegeDAL.findOne({ + id: selector.id, + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + if (!additionalPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const getAdditionalPrivilegeByName = async (dto: TGetAdditionalPrivilegesByNameDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onGetAdditionalPrivilegesByIdGuard(dto); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + const scope = factory.getScopeField(dto.scopeData); + + const additionalPrivilege = await additionalPrivilegeDAL.findOne({ + name: selector.name, + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + if (!additionalPrivilege) + throw new NotFoundError({ message: `Additional privilege with name ${selector.name} doesn't exist` }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const listAdditionalPrivileges = async (dto: TListAdditionalPrivilegesDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onListAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const additionalPrivileges = await additionalPrivilegeDAL.find({ + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + + return { + additionalPrivileges: additionalPrivileges.map((el) => ({ + ...el, + permissions: unpackPermissions(el.permissions) + })) + }; + }; + + return { + createAdditionalPrivilege, + updateAdditionalPrivilege, + deleteAdditionalPrivilege, + getAdditionalPrivilegeById, + getAdditionalPrivilegeByName, + listAdditionalPrivileges + }; +}; diff --git a/backend/src/services/additional-privilege/additional-privilege-types.ts b/backend/src/services/additional-privilege/additional-privilege-types.ts new file mode 100644 index 0000000000..eeb5a6a0bd --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-types.ts @@ -0,0 +1,87 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +import { ActorType } from "../auth/auth-type"; + +export interface TAdditionalPrivilegesScopeFactory { + onCreateAdditionalPrivilegesGuard: (arg: TCreateAdditionalPrivilegesDTO) => Promise; + onUpdateAdditionalPrivilegesGuard: (arg: TUpdateAdditionalPrivilegesDTO) => Promise; + onDeleteAdditionalPrivilegesGuard: (arg: TDeleteAdditionalPrivilegesDTO) => Promise; + onListAdditionalPrivilegesGuard: (arg: TListAdditionalPrivilegesDTO) => Promise; + onGetAdditionalPrivilegesByIdGuard: ( + arg: TGetAdditionalPrivilegesByIdDTO | TGetAdditionalPrivilegesByNameDTO + ) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; +} + +export type TCreateAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + name: string; + permissions: unknown; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }; +}; + +export type TUpdateAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; + data: Partial<{ + name: string; + permissions: unknown; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }>; +}; + +export type TListAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; + +export type TDeleteAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; + +export type TGetAdditionalPrivilegesByIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; + +export type TGetAdditionalPrivilegesByNameDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + name: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; diff --git a/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts b/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts new file mode 100644 index 0000000000..e2ccde286c --- /dev/null +++ b/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts @@ -0,0 +1,52 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TNamespaceAdditionalPrivilegesScopeFactoryDep = Record; + +export const newNamespaceAdditionalPrivilegesFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceAdditionalPrivilegesScopeFactoryDep +): TAdditionalPrivilegesScopeFactory => { + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges create not implemented" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges update not implemented" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges delete not implemented" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges list not implemented" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges get by id not implemented" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts b/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts new file mode 100644 index 0000000000..cc1fbdd6c3 --- /dev/null +++ b/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts @@ -0,0 +1,52 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TOrgAdditionalPrivilegesScopeFactoryDep = Record; + +export const newOrgAdditionalPrivilegesFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TOrgAdditionalPrivilegesScopeFactoryDep +): TAdditionalPrivilegesScopeFactory => { + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges create not implemented" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges update not implemented" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges delete not implemented" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges list not implemented" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges get by id not implemented" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts b/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts new file mode 100644 index 0000000000..4b5179a6f0 --- /dev/null +++ b/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts @@ -0,0 +1,219 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionIdentityActions, + ProjectPermissionMemberActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, PermissionBoundaryError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TMembershipDALFactory } from "@app/services/membership/membership-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TProjectAdditionalPrivilegesScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + membershipDAL: Pick; +}; + +export const newProjectAdditionalPrivilegesFactory = ({ + permissionService, + orgDAL, + membershipDAL +}: TProjectAdditionalPrivilegesScopeFactoryDep): TAdditionalPrivilegesScopeFactory => { + const $getPermission = (permission: OrgServiceActor, projectId: string) => { + return permissionService.getProjectPermission({ + actor: permission.type, + actorId: permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: permission.authMethod, + projectId, + actorOrgId: permission.orgId + }); + }; + + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + + const { actorType } = dto.data; + const { permission } = await $getPermission(dto.permission, scope.value); + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Edit, ProjectPermissionSub.Identity] as const); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const { permission: targetUserPermission, memberships } = await $getPermission( + { ...dto.permission, type: actorType, id: dto.data.actorId }, + scope.value + ); + + const permissionAction = + actorType === ActorType.USER + ? ProjectPermissionMemberActions.GrantPrivileges + : ProjectPermissionIdentityActions.GrantPrivileges; + const permissionSubject = + actorType === ActorType.USER ? ProjectPermissionSub.Member : ProjectPermissionSub.Identity; + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged actor", + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const membership = memberships.find( + (el) => el[actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"] === dto.data.actorId + ); + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const { permission } = await $getPermission(dto.permission, scope.value); + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Edit, ProjectPermissionSub.Identity] as const); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const { permission: targetUserPermission, memberships } = await $getPermission( + { ...dto.permission, type: actorType, id: dto.selector.actorId }, + scope.value + ); + + const permissionAction = + actorType === ActorType.USER + ? ProjectPermissionMemberActions.GrantPrivileges + : ProjectPermissionIdentityActions.GrantPrivileges; + const permissionSubject = + actorType === ActorType.USER ? ProjectPermissionSub.Member : ProjectPermissionSub.Identity; + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged actor", + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const membership = memberships.find( + (el) => el[actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"] === dto.selector.actorId + ); + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const { permission } = await $getPermission(dto.permission, scope.value); + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Edit, ProjectPermissionSub.Identity] as const); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity] as const); + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity] as const); + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/app-connection/railway/railway-connection-public-client.ts b/backend/src/services/app-connection/railway/railway-connection-public-client.ts index 1c8bd9cc24..47eb3f3967 100644 --- a/backend/src/services/app-connection/railway/railway-connection-public-client.ts +++ b/backend/src/services/app-connection/railway/railway-connection-public-client.ts @@ -75,7 +75,7 @@ class RailwayPublicClient { async send( query: string, options: RailwaySendReqOptions, - variables: Record> = {}, + variables: Record = {}, retryAttempt: number = 0 ): Promise { const body = { @@ -117,6 +117,25 @@ class RailwayPublicClient { } } + async getDeployments( + config: RailwaySendReqOptions, + variables: { input: { serviceId: string; environmentId: string }; first?: number } + ) { + return this.send>( + `query deployments($input: DeploymentListInput!, $first: Int) { deployments(first: $first, input: $input) { edges { node { id } } } }`, + config, + variables + ); + } + + async redeployDeployment(config: RailwaySendReqOptions, variables: { input: { deploymentId: string } }) { + return this.send>( + `mutation deploymentRedeploy($deploymentId: String!) { deploymentRedeploy(id: $deploymentId) { id } }`, + config, + { deploymentId: variables.input.deploymentId } + ); + } + async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) { const res = await this.send( `query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`, @@ -213,7 +232,9 @@ class RailwayPublicClient { async deleteVariable( config: RailwaySendReqOptions, - variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } } + variables: { + input: { projectId: string; environmentId: string; name: string; skipDeploys?: boolean; serviceId?: string }; + } ) { await this.send }>>( `mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`, @@ -222,6 +243,26 @@ class RailwayPublicClient { ); } + async upsertCollection( + config: RailwaySendReqOptions, + variables: { + input: { + projectId: string; + environmentId: string; + variables: Record; + skipDeploys?: boolean; + serviceId?: string; + replace?: boolean; + }; + } + ) { + return this.send>( + `mutation variableCollectionUpsert($input: VariableCollectionUpsertInput!) { variableCollectionUpsert(input: $input) }`, + config, + variables + ); + } + async upsertVariable( config: RailwaySendReqOptions, variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } } diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 613aa0766a..82df0dcb11 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,12 +1,12 @@ import { Knex } from "knex"; -import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; +import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -14,7 +14,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; - orgMembershipDAL: Pick; + membershipUserDAL: Pick; }; export type TAuthTokenServiceFactory = ReturnType; @@ -80,7 +80,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -208,9 +208,10 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); if (token.organizationId) { - const orgMembership = await orgMembershipDAL.findOne({ - userId: user.id, - orgId: token.organizationId + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: token.organizationId, + scope: AccessScope.Organization }); if (!orgMembership) { diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index d69c836e98..31a9cc5a80 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,6 +1,13 @@ import { Knex } from "knex"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { + AccessScope, + OrgMembershipRole, + OrgMembershipStatus, + TableName, + TUsers, + UserDeviceSchema +} from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; @@ -14,11 +21,12 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TNotificationServiceFactory } from "../notification/notification-service"; import { NotificationType } from "../notification/notification-types"; import { TOrgDALFactory } from "../org/org-dal"; import { getDefaultOrgMembershipRole } from "../org/org-role-fns"; -import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { LoginMethod } from "../super-admin/super-admin-types"; import { TTotpServiceFactory } from "../totp/totp-service"; @@ -48,7 +56,8 @@ type TAuthLoginServiceFactoryDep = { smtpService: TSmtpService; totpService: Pick; auditLogService: Pick; - orgMembershipDAL: TOrgMembershipDALFactory; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; notificationService: Pick; }; @@ -58,10 +67,11 @@ export const authLoginServiceFactory = ({ tokenService, smtpService, orgDAL, - orgMembershipDAL, totpService, auditLogService, - notificationService + notificationService, + membershipUserDAL, + membershipRoleDAL }: TAuthLoginServiceFactoryDep) => { /* * Private @@ -163,8 +173,8 @@ export const authLoginServiceFactory = ({ if (organizationId) { const org = await orgDAL.findById(organizationId); if (org) { - await orgMembershipDAL.update( - { userId: user.id, orgId: org.id }, + await membershipUserDAL.update( + { actorUserId: user.id, scopeOrgId: org.id, scope: AccessScope.Organization }, { lastLoginAuthMethod: authMethod, lastLoginTime: new Date() } ); if (org.userTokenExpiration) { @@ -858,21 +868,34 @@ export const authLoginServiceFactory = ({ } orgId = defaultOrg.id; const [orgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole); - await orgMembershipDAL.create({ - userId: user.id, - inviteEmail: email, - orgId, - role, - roleId, - status: OrgMembershipStatus.Accepted, - isActive: true + await membershipUserDAL.transaction(async (tx) => { + const membership = await membershipUserDAL.create( + { + actorUserId: user?.id, + inviteEmail: email, + scopeOrgId: orgId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted, + isActive: true + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); }); } } @@ -895,10 +918,11 @@ export const authLoginServiceFactory = ({ if (org) { // checks for the membership and only sets the orgId / orgName if the user is a member of the specified org const orgMembership = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "orgId"]: org.id, - [`${TableName.OrgMembership}.isActive` as "isActive"]: true, - [`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: org.id, + [`${TableName.Membership}.isActive` as "isActive"]: true, + [`${TableName.Membership}.status` as "status"]: OrgMembershipStatus.Accepted, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (orgMembership) { diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 21a51ef3f3..75f77d0fc7 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -1,3 +1,4 @@ +import { AccessScope } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; @@ -6,7 +7,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; -import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; @@ -23,7 +24,7 @@ import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; userDAL: TUserDALFactory; - orgMembershipDAL: Pick; + membershipUserDAL: Pick; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; totpConfigDAL: Pick; @@ -33,7 +34,7 @@ export type TAuthPasswordFactory = ReturnType; export const authPaswordServiceFactory = ({ authDAL, userDAL, - orgMembershipDAL, + membershipUserDAL, tokenService, smtpService, totpConfigDAL @@ -54,7 +55,10 @@ export const authPaswordServiceFactory = ({ const hasEmailAuth = user.authMethods?.includes(AuthMethod.EMAIL); if (!hasEmailAuth) { - const orgMemberships = await orgMembershipDAL.find({ userId: user.id }); + const orgMemberships = await membershipUserDAL.find({ + actorUserId: user.id, + scope: AccessScope.Organization + }); const lastLoginMethod = orgMemberships .filter((membership) => membership.lastLoginAuthMethod) diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 7bc3a5ef68..a2e426a2ea 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,4 +1,4 @@ -import { OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName } from "@app/db/schemas"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -8,17 +8,15 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { getMinExpiresIn } from "@app/lib/fn"; import { isDisposableEmail } from "@app/lib/validator"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; 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 { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TMembershipGroupDALFactory } from "../membership-group/membership-group-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { getServerCfg } from "../super-admin/super-admin-service"; import { TUserDALFactory } from "../user/user-dal"; @@ -42,14 +40,12 @@ type TAuthSignupDep = { projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; - groupProjectDAL: Pick; orgService: Pick; orgDAL: TOrgDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; licenseService: Pick; - projectMembershipDAL: Pick; - projectUserMembershipRoleDAL: Pick; + membershipGroupDAL: TMembershipGroupDALFactory; }; export type TAuthSignupFactory = ReturnType; @@ -60,11 +56,11 @@ export const authSignupServiceFactory = ({ projectKeyDAL, projectDAL, projectBotDAL, - groupProjectDAL, tokenService, smtpService, orgService, orgDAL, + membershipGroupDAL, licenseService }: TAuthSignupDep) => { // first step of signup. create user and send email @@ -200,9 +196,10 @@ export const authSignupServiceFactory = ({ organizationId ) { const [pendingOrgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, status: OrgMembershipStatus.Invited, - [`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: organizationId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (pendingOrgMembership) { @@ -241,18 +238,18 @@ export const authSignupServiceFactory = ({ } const updatedMembersips = await orgDAL.updateMembership( - { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, - { userId: user.id, status: OrgMembershipStatus.Accepted } + { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization }, + { actorUserId: user.id, status: OrgMembershipStatus.Accepted } ); - const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; + const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))]; await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))); await convertPendingGroupAdditionsToGroupMemberships({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, + membershipGroupDAL, projectDAL, projectBotDAL }); @@ -351,21 +348,21 @@ export const authSignupServiceFactory = ({ ); const updatedMembersips = await orgDAL.updateMembership( - { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, - { userId: us.id, status: OrgMembershipStatus.Accepted }, + { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization }, + { actorUserId: us.id, status: OrgMembershipStatus.Accepted }, tx ); - const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; + const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))]; await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx))); await convertPendingGroupAdditionsToGroupMemberships({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); diff --git a/backend/src/services/convertor/convertor-service.ts b/backend/src/services/convertor/convertor-service.ts new file mode 100644 index 0000000000..71c25e9c07 --- /dev/null +++ b/backend/src/services/convertor/convertor-service.ts @@ -0,0 +1,130 @@ +import { AccessScope } from "@app/db/schemas"; +import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; +import { NotFoundError } from "@app/lib/errors"; + +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TMembershipDALFactory } from "../membership/membership-dal"; +import { TProjectDALFactory } from "../project/project-dal"; + +type TConvertorServiceFactoryDep = { + projectDAL: Pick; + membershipDAL: Pick; + groupDAL: Pick; + additionalPrivilegeDAL: Pick; +}; + +export type TConvertorServiceFactory = ReturnType; + +export const convertorServiceFactory = ({ + projectDAL, + membershipDAL, + additionalPrivilegeDAL, + groupDAL +}: TConvertorServiceFactoryDep) => { + const projectSlugToId = async (dto: { slug: string; orgId: string }) => { + const project = await projectDAL.findOne({ + orgId: dto.orgId, + slug: dto.slug + }); + if (!project) throw new NotFoundError({ message: `Project with slug ${dto.slug} not found` }); + return project; + }; + + const userMembershipIdToUserId = async (membershipId: string, scope: AccessScope, orgId: string) => { + const membership = await membershipDAL.findOne({ + scope, + id: membershipId, + scopeOrgId: orgId + }); + if (!membership || !membership.actorUserId) { + throw new NotFoundError({ message: `Membership with id ${membershipId} not found` }); + } + return { userId: membership.actorUserId, membership }; + }; + + const groupMembershipIdToGroupId = async (membershipId: string, scope: AccessScope, orgId: string) => { + const membership = await membershipDAL.findOne({ + scope, + id: membershipId, + scopeOrgId: orgId + }); + if (!membership || !membership.actorGroupId) { + throw new NotFoundError({ message: `Membership with id ${membershipId} not found` }); + } + + return { groupId: membership.actorGroupId, membership }; + }; + + const identityMembershipIdToIdentityId = async (membershipId: string, scope: AccessScope, orgId: string) => { + const membership = await membershipDAL.findOne({ + scope, + id: membershipId, + scopeOrgId: orgId + }); + if (!membership || !membership.actorIdentityId) { + throw new NotFoundError({ message: `Membership with id ${membershipId} not found` }); + } + + return { identityId: membership.actorIdentityId, membership }; + }; + + const identityIdToMembershipId = async (identityId: string, scope: AccessScope, scopeId: string) => { + let fieldName = "scopeOrgId"; + if (scope === AccessScope.Project) { + fieldName = "scopeProjectId"; + } else if (scope === AccessScope.Namespace) { + fieldName = "scopeNamespaceId"; + } + + const membership = await membershipDAL.findOne({ + scope, + actorIdentityId: identityId, + [fieldName]: scopeId + }); + + if (!membership) { + throw new NotFoundError({ message: `Identity with id ${identityId} not found` }); + } + + return { membershipId: membership.id, membership }; + }; + + const additionalPrivilegeIdToDoc = async (privilegeId: string) => { + const doc = await additionalPrivilegeDAL.findOne({ + id: privilegeId + }); + if (!doc) { + throw new NotFoundError({ message: `Privilege with id ${privilegeId} not found` }); + } + + return { privilege: doc }; + }; + const additionalPrivilegeNameToDoc = async (privilegeName: string, projectId: string) => { + const privilege = await additionalPrivilegeDAL.findOne({ + name: privilegeName, + projectId + }); + if (!privilege) { + throw new NotFoundError({ message: `Privilege with name ${privilegeName} not found` }); + } + + return { privilegeId: privilege.id, privilege }; + }; + + const getGroupIdFromName = async (name: string, orgId: string) => { + const group = await groupDAL.findOne({ orgId, name }); + if (!group) throw new NotFoundError({ message: `Failed to find group with name ${name}` }); + return { groupId: group.id, group }; + }; + + return { + projectSlugToId, + userMembershipIdToUserId, + groupMembershipIdToGroupId, + identityMembershipIdToIdentityId, + additionalPrivilegeIdToDoc, + additionalPrivilegeNameToDoc, + identityIdToMembershipId, + getGroupIdFromName + }; +}; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts index 552eed3075..21b0b3c1f6 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts @@ -1,19 +1,19 @@ -import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { isCustomOrgRole } from "@app/services/org/org-role-fns"; +import { TRoleDALFactory } from "../role/role-dal"; import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types"; export const constructGroupOrgMembershipRoleMappings = async ({ mappingsDTO, orgId, - orgRoleDAL, + roleDAL, licenseService }: { mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[]; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; licenseService: TLicenseServiceFactory; orgId: string; }) => { @@ -30,9 +30,9 @@ export const constructGroupOrgMembershipRoleMappings = async ({ .filter((mapping) => isCustomOrgRole(mapping.roleSlug)) .map((mapping) => mapping.roleSlug); - let customRolesMap: Map = new Map(); + let customRolesMap: Map = new Map(); if (customRoleSlugs.length > 0) { - const customRoles = await orgRoleDAL.find({ + const customRoles = await roleDAL.find({ orgId, $in: { slug: customRoleSlugs diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts index de293609b8..a072544ca7 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts @@ -6,15 +6,15 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { OrgServiceActor } from "@app/lib/types"; import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns"; import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { TRoleDALFactory } from "../role/role-dal"; import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal"; type TExternalGroupOrgRoleMappingServiceFactoryDep = { externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; permissionService: TPermissionServiceFactory; licenseService: TLicenseServiceFactory; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; }; export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType; @@ -23,7 +23,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ externalGroupOrgRoleMappingDAL, licenseService, permissionService, - orgRoleDAL + roleDAL }: TExternalGroupOrgRoleMappingServiceFactoryDep) => { const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => { const { permission } = await permissionService.getOrgPermission( @@ -61,7 +61,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ const mappings = await constructGroupOrgMembershipRoleMappings({ mappingsDTO: dto.mappings, - orgRoleDAL, + roleDAL, licenseService, orgId: actor.orgId }); diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index e801b607e9..14885b1d62 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -47,14 +47,14 @@ export const externalMigrationServiceFactory = ({ throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." }); } - const { membership } = await permissionService.getOrgPermission( + const { hasRole } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); } @@ -94,7 +94,7 @@ export const externalMigrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TImportVaultDataDTO) => { - const { membership } = await permissionService.getOrgPermission( + const { hasRole } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -102,7 +102,7 @@ export const externalMigrationServiceFactory = ({ actorOrgId ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); } @@ -150,7 +150,7 @@ export const externalMigrationServiceFactory = ({ actorAuthMethod, provider }: THasCustomVaultMigrationDTO) => { - const { membership } = await permissionService.getOrgPermission( + const { hasRole } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -158,7 +158,7 @@ export const externalMigrationServiceFactory = ({ actorOrgId ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" }); } diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index 263838a9da..4d14758dc7 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -1,53 +1,44 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TMemberships, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, sqlNestRelationships } from "@app/lib/knex"; +import { sqlNestRelationships } from "@app/lib/knex"; export type TGroupProjectDALFactory = ReturnType; export const groupProjectDALFactory = (db: TDbClient) => { - const groupProjectOrm = ormify(db, TableName.GroupProjectMembership); - const findByProjectId = async (projectId: string, filter?: { groupId?: string }, tx?: Knex) => { try { - const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + const docs = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorGroupId`) .where((qb) => { if (filter?.groupId) { void qb.where(`${TableName.Groups}.id`, "=", filter.groupId); } }) - .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) - .join( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.Groups, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.GroupProjectMembership), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), db.ref("id").as("groupId").withSchema(TableName.Groups), db.ref("name").as("groupName").withSchema(TableName.Groups), db.ref("slug").as("groupSlug").withSchema(TableName.Groups), - db.ref("id").withSchema(TableName.GroupProjectMembership), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole) + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole) ); const members = sqlNestRelationships({ @@ -104,8 +95,8 @@ export const groupProjectDALFactory = (db: TDbClient) => { try { const docs = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.userId`, userId) - .join(TableName.Groups, function () { - this.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn( + .join(TableName.Groups, (qb) => { + qb.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn( `${TableName.Groups}.orgId`, "=", db.raw("?", [orgId]) @@ -131,32 +122,26 @@ export const groupProjectDALFactory = (db: TDbClient) => { const docs = await db(TableName.UserGroupMembership) // Join the GroupProjectMembership table with the Groups table to get the group name and slug. .join( - TableName.GroupProjectMembership, + TableName.Membership, `${TableName.UserGroupMembership}.groupId`, - `${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership + `${TableName.Membership}.actorGroupId` // this gives us access to the project id in the group membership ) - - .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) - - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) - + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .join( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) - .join( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .join(TableName.OrgMembership, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .join(db(TableName.Membership).as("orgMembership"), (qb) => { + qb.on(`${TableName.Users}.id`, `orgMembership.actorUserId`) + .andOn(`orgMembership.scope`, db.raw("?", [AccessScope.Organization])) + .andOn(`orgMembership.scopeOrgId`, `${TableName.Project}.orgId`); + }) .select( db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("createdAt").withSchema(TableName.UserGroupMembership), @@ -167,18 +152,18 @@ export const groupProjectDALFactory = (db: TDbClient) => { db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), - db.ref("isActive").withSchema(TableName.OrgMembership) + db.ref("isActive").withSchema("orgMembership") ) .where({ isGhost: false }); @@ -242,5 +227,5 @@ export const groupProjectDALFactory = (db: TDbClient) => { return members; }; - return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers }; + return { findByProjectId, findByUserId, findAllProjectGroupMembers }; }; diff --git a/backend/src/services/group-project/group-project-membership-role-dal.ts b/backend/src/services/group-project/group-project-membership-role-dal.ts deleted file mode 100644 index 5572ac6f51..0000000000 --- a/backend/src/services/group-project/group-project-membership-role-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TGroupProjectMembershipRoleDALFactory = ReturnType; - -export const groupProjectMembershipRoleDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.GroupProjectMembershipRole); - return orm; -}; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 913aca2f34..4a4a614967 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,507 +1,27 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; -import { - constructPermissionErrorMessage, - validatePrivilegeChangeOperation -} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; -import { ms } from "@app/lib/ms"; -import { isUuidV4 } from "@app/lib/validator"; +import { NotFoundError } from "@app/lib/errors"; import { TGroupDALFactory } from "../../ee/services/group/group-dal"; -import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; -import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; -import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; -import { TGroupProjectDALFactory } from "./group-project-dal"; -import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membership-role-dal"; -import { - TCreateProjectGroupDTO, - TDeleteProjectGroupDTO, - TGetGroupInProjectDTO, - TListProjectGroupDTO, - TUpdateProjectGroupDTO -} from "./group-project-types"; type TGroupProjectServiceFactoryDep = { - groupProjectDAL: Pick; - groupProjectMembershipRoleDAL: Pick< - TGroupProjectMembershipRoleDALFactory, - "create" | "transaction" | "insertMany" | "delete" - >; - userGroupMembershipDAL: Pick; - projectDAL: Pick; - projectKeyDAL: Pick; - projectRoleDAL: Pick; - projectBotDAL: TProjectBotDALFactory; groupDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" - >; + projectDAL: Pick; + permissionService: Pick; }; export type TGroupProjectServiceFactory = ReturnType; export const groupProjectServiceFactory = ({ groupDAL, - groupProjectDAL, - groupProjectMembershipRoleDAL, - userGroupMembershipDAL, projectDAL, - projectKeyDAL, - projectBotDAL, - projectRoleDAL, permissionService }: TGroupProjectServiceFactoryDep) => { - const addGroupToProject = async ({ - actor, - actorId, - actorOrgId, - actorAuthMethod, - roles, - projectId, - groupIdOrName - }: TCreateProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); - - let group: TGroups | null = null; - if (isUuidV4(groupIdOrName)) { - group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName }); - } - if (!group) { - group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName }); - } - - if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` }); - - const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (existingGroup) - throw new BadRequestError({ - message: `Group with ID ${group.id} already exists in project with id ${project.id}` - }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - project.id - ); - - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign group to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId: project.id, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - - if (customRoles.length !== customInputRoles.length) { - const customRoleSlugs = customRoles.map((customRole) => customRole.slug); - const missingInputRoles = customInputRoles - .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) - .map((role) => role.role); - - throw new NotFoundError({ - message: `Custom role/s not found: ${missingInputRoles.join(", ")}` - }); - } - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const projectGroup = await groupProjectDAL.transaction(async (tx) => { - const groupProjectMembership = await groupProjectDAL.create( - { - groupId: group!.id, - projectId: project.id - }, - tx - ); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: groupProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: groupProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - await groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - - // share project key with users in group that have not - // individually been added to the project and that are not part of - // other groups that are in the project - const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); - - if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) { - const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); - - if (!ghostUser) { - throw new NotFoundError({ - message: `Failed to find project owner of project with name ${project.name}` - }); - } - - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id, tx); - - if (!ghostUserLatestKey) { - throw new NotFoundError({ - message: `Failed to find project owner's latest key in project with name ${project.name}` - }); - } - - if (!ghostUserLatestKey.sender.publicKey) { - throw new NotFoundError({ - message: `Failed to find project owner's latest key in project with name ${project.name}` - }); - } - - const bot = await projectBotDAL.findOne({ projectId: project.id }, tx); - - if (!bot) { - throw new NotFoundError({ - message: `Failed to find project bot in project with name ${project.name}` - }); - } - - const botPrivateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - - const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({ - ciphertext: ghostUserLatestKey.encryptedKey, - nonce: ghostUserLatestKey.nonce, - publicKey: ghostUserLatestKey.sender.publicKey, - privateKey: botPrivateKey - }); - - const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { - if (!publicKey) { - throw new NotFoundError({ - message: `Failed to find user's public key in project with name ${project.name}` - }); - } - - const { ciphertext: encryptedKey, nonce } = crypto - .encryption() - .asymmetric() - .encrypt(plaintextProjectKey, publicKey, botPrivateKey); - - return { - encryptedKey, - nonce, - senderId: ghostUser.id, - receiverId: id, - projectId: project.id - }; - }); - - await projectKeyDAL.insertMany(projectKeyData, tx); - } - - return groupProjectMembership; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return projectGroup; - }; - - const updateGroupInProject = async ({ - projectId, - groupId, - roles, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TUpdateProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); - - const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); - if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!projectGroup) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - project.id - ); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign group to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId: project.id, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) { - const customRoleSlugs = customRoles.map((customRole) => customRole.slug); - const missingInputRoles = customInputRoles - .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) - .map((role) => role.role); - - throw new NotFoundError({ - message: `Custom role/s not found: ${missingInputRoles.join(", ")}` - }); - } - - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: projectGroup.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: projectGroup.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => { - await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx); - return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return updatedRoles; - }; - - const removeGroupFromProject = async ({ - projectId, - groupId, - actorId, - actor, - actorOrgId, - actorAuthMethod - }: TDeleteProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - - const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); - if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!groupProjectMembership) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); - - const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { - const groupMembersNotInProject = await userGroupMembershipDAL.findGroupMembersNotInProject( - group.id, - project.id, - tx - ); - - if (groupMembersNotInProject.length) { - await projectKeyDAL.delete( - { - projectId: project.id, - $in: { - receiverId: groupMembersNotInProject.map(({ user: { id } }) => id) - } - }, - tx - ); - } - - const [projectGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id }, tx); - return projectGroup; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return deletedProjectGroup; - }; - - const listGroupsInProject = async ({ - projectId, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TListProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) { - throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - - const groupMemberships = await groupProjectDAL.findByProjectId(project.id); - return groupMemberships; - }; - - const getGroupInProject = async ({ - actor, - actorId, - actorAuthMethod, - actorOrgId, - groupId, - projectId - }: TGetGroupInProjectDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) { - throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - - const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { - groupId - }); - - if (!groupMembership) { - throw new NotFoundError({ - message: `Group membership with ID ${groupId} not found in project with ID ${projectId}` - }); - } - - return groupMembership; - }; - const listProjectGroupUsers = async ({ id, projectId, @@ -545,11 +65,6 @@ export const groupProjectServiceFactory = ({ }; return { - addGroupToProject, - updateGroupInProject, - removeGroupFromProject, - listGroupsInProject, - getGroupInProject, listProjectGroupUsers }; }; diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 71f7ec3654..1b230f7a57 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -204,7 +204,7 @@ export const identityAccessTokenServiceFactory = ({ } const identityOrgMembership = await identityOrgMembershipDAL.findOne({ - identityId: identityAccessToken.identityId + actorIdentityId: identityAccessToken.identityId }); if (!identityOrgMembership) { @@ -219,7 +219,7 @@ export const identityAccessTokenServiceFactory = ({ await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); - return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; + return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId }; }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index af94c79c9b..43584a1afb 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -18,9 +18,10 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAliCloudAuthDALFactory } from "./identity-alicloud-auth-dal"; import { @@ -38,9 +39,10 @@ type TIdentityAliCloudAuthServiceFactoryDep = { TIdentityAliCloudAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; + orgDAL: Pick; }; export type TIdentityAliCloudAuthServiceFactory = ReturnType; @@ -48,9 +50,10 @@ export type TIdentityAliCloudAuthServiceFactory = ReturnType { const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => { const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); @@ -60,8 +63,9 @@ export const identityAliCloudAuthServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityAliCloudAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAliCloudAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); @@ -89,7 +93,7 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, @@ -150,7 +154,13 @@ export const identityAliCloudAuthServiceFactory = ({ }: TAttachAliCloudAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { @@ -166,13 +176,13 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -193,7 +203,7 @@ export const identityAliCloudAuthServiceFactory = ({ const identityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => { const doc = await identityAliCloudAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type: "iam", allowedArns, accessTokenMaxTTL, @@ -205,7 +215,7 @@ export const identityAliCloudAuthServiceFactory = ({ ); return doc; }); - return { ...identityAliCloudAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAliCloudAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateAliCloudAuth = async ({ @@ -220,7 +230,13 @@ export const identityAliCloudAuthServiceFactory = ({ actor, actorOrgId }: TUpdateAliCloudAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { @@ -242,13 +258,13 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -276,11 +292,17 @@ export const identityAliCloudAuthServiceFactory = ({ : undefined }); - return { ...updatedAliCloudAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedAliCloudAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getAliCloudAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAliCloudAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { @@ -294,12 +316,12 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityAliCloudAuth = async ({ @@ -309,17 +331,23 @@ export const identityAliCloudAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeAliCloudAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ message: "The identity does not have Alibaba Cloud auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -327,14 +355,15 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -345,7 +374,7 @@ export const identityAliCloudAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke Alibaba Cloud auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -356,7 +385,7 @@ export const identityAliCloudAuthServiceFactory = ({ const deletedAliCloudAuth = await identityAliCloudAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.ALICLOUD_AUTH }, tx); - return { ...deletedAliCloudAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedAliCloudAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityAliCloudAuth; }; 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 3dff474033..8793c3a007 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 @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import axios from "axios"; import RE2 from "re2"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -17,9 +17,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal"; import { extractPrincipalArn, extractPrincipalArnEntity } from "./identity-aws-auth-fns"; @@ -36,9 +37,10 @@ import { type TIdentityAwsAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; + orgDAL: Pick; }; export type TIdentityAwsAuthServiceFactory = ReturnType; @@ -80,9 +82,10 @@ function isValidAwsRegion(region: string | null): boolean { export const identityAwsAuthServiceFactory = ({ identityAccessTokenDAL, identityAwsAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, licenseService, - permissionService + permissionService, + orgDAL }: TIdentityAwsAuthServiceFactoryDep) => { const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => { const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); @@ -90,7 +93,10 @@ export const identityAwsAuthServiceFactory = ({ throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAwsAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAwsAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); @@ -153,7 +159,7 @@ export const identityAwsAuthServiceFactory = ({ } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, @@ -226,7 +232,13 @@ export const identityAwsAuthServiceFactory = ({ }: TAttachAwsAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { @@ -242,13 +254,13 @@ export const identityAwsAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -269,7 +281,7 @@ export const identityAwsAuthServiceFactory = ({ const identityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { const doc = await identityAwsAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type: "iam", stsEndpoint, allowedPrincipalArns, @@ -283,7 +295,7 @@ export const identityAwsAuthServiceFactory = ({ ); return doc; }); - return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAwsAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateAwsAuth = async ({ @@ -300,7 +312,13 @@ export const identityAwsAuthServiceFactory = ({ actor, actorOrgId }: TUpdateAwsAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { @@ -321,13 +339,13 @@ export const identityAwsAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -357,11 +375,17 @@ export const identityAwsAuthServiceFactory = ({ : undefined }); - return { ...updatedAwsAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedAwsAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { @@ -375,12 +399,12 @@ export const identityAwsAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...awsIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityAwsAuth = async ({ @@ -390,17 +414,23 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeAwsAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ message: "The identity does not have aws auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -408,14 +438,15 @@ export const identityAwsAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -426,7 +457,7 @@ export const identityAwsAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke aws auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -437,7 +468,7 @@ export const identityAwsAuthServiceFactory = ({ const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AWS_AUTH }, tx); - return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityAwsAuth; }; 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 9a2426a7c5..b3250ed56e 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 @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -14,9 +14,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal"; import { validateAzureIdentity } from "./identity-azure-auth-fns"; @@ -33,20 +34,22 @@ type TIdentityAzureAuthServiceFactoryDep = { TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; }; export type TIdentityAzureAuthServiceFactory = ReturnType; export const identityAzureAuthServiceFactory = ({ identityAzureAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + orgDAL }: TIdentityAzureAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => { const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); @@ -54,7 +57,10 @@ export const identityAzureAuthServiceFactory = ({ throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAzureAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAzureAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); const azureIdentity = await validateAzureIdentity({ @@ -80,7 +86,7 @@ export const identityAzureAuthServiceFactory = ({ } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, @@ -139,7 +145,13 @@ export const identityAzureAuthServiceFactory = ({ }: TAttachAzureAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { @@ -154,13 +166,13 @@ export const identityAzureAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -181,7 +193,7 @@ export const identityAzureAuthServiceFactory = ({ const identityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { const doc = await identityAzureAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, tenantId, resource, allowedServicePrincipalIds, @@ -195,7 +207,7 @@ export const identityAzureAuthServiceFactory = ({ return doc; }); - return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateAzureAuth = async ({ @@ -212,7 +224,13 @@ export const identityAzureAuthServiceFactory = ({ actor, actorOrgId }: TUpdateAzureAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ @@ -232,13 +250,13 @@ export const identityAzureAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -270,12 +288,18 @@ export const identityAzureAuthServiceFactory = ({ return { ...updatedAzureAuth, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ @@ -288,13 +312,13 @@ export const identityAzureAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityAzureAuth = async ({ @@ -304,17 +328,23 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeAzureAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have azure auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -322,13 +352,14 @@ export const identityAzureAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -338,7 +369,7 @@ export const identityAzureAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke azure auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -349,7 +380,7 @@ export const identityAzureAuthServiceFactory = ({ const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AZURE_AUTH }, tx); - return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityAzureAuth; }; 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 388c24d484..fe7b9b6d70 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 @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -14,9 +14,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal"; import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns"; @@ -31,20 +32,22 @@ import { type TIdentityGcpAuthServiceFactoryDep = { identityGcpAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; }; export type TIdentityGcpAuthServiceFactory = ReturnType; export const identityGcpAuthServiceFactory = ({ identityGcpAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + orgDAL }: TIdentityGcpAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => { const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); @@ -52,7 +55,10 @@ export const identityGcpAuthServiceFactory = ({ throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityGcpAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) { throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); } @@ -119,7 +125,7 @@ export const identityGcpAuthServiceFactory = ({ } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, @@ -179,7 +185,13 @@ export const identityGcpAuthServiceFactory = ({ }: TAttachGcpAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -195,13 +207,13 @@ export const identityGcpAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -222,7 +234,7 @@ export const identityGcpAuthServiceFactory = ({ const identityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { const doc = await identityGcpAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type, allowedServiceAccounts, allowedProjects, @@ -236,7 +248,7 @@ export const identityGcpAuthServiceFactory = ({ ); return doc; }); - return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateGcpAuth = async ({ @@ -254,7 +266,13 @@ export const identityGcpAuthServiceFactory = ({ actor, actorOrgId }: TUpdateGcpAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -275,13 +293,13 @@ export const identityGcpAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -314,12 +332,18 @@ export const identityGcpAuthServiceFactory = ({ return { ...updatedGcpAuth, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -333,13 +357,13 @@ export const identityGcpAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityGcpAuth = async ({ @@ -349,7 +373,13 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeGcpAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -357,10 +387,10 @@ export const identityGcpAuthServiceFactory = ({ message: "The identity does not have gcp auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -368,13 +398,14 @@ export const identityGcpAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -384,7 +415,7 @@ export const identityGcpAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke gcp auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -395,7 +426,7 @@ export const identityGcpAuthServiceFactory = ({ const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.GCP_AUTH }, tx); - return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityGcpAuth; }; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 9cc851437c..a99c8ad78d 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -3,7 +3,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -24,11 +24,12 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal"; import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns"; @@ -43,22 +44,24 @@ import { type TIdentityJwtAuthServiceFactoryDep = { identityJwtAuthDAL: TIdentityJwtAuthDALFactory; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; + orgDAL: Pick; }; export type TIdentityJwtAuthServiceFactory = ReturnType; export const identityJwtAuthServiceFactory = ({ identityJwtAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, permissionService, licenseService, identityAccessTokenDAL, - kmsService + kmsService, + orgDAL }: TIdentityJwtAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); @@ -66,8 +69,9 @@ export const identityJwtAuthServiceFactory = ({ throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityJwtAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityJwtAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { throw new NotFoundError({ @@ -77,7 +81,7 @@ export const identityJwtAuthServiceFactory = ({ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }); @@ -207,7 +211,7 @@ export const identityJwtAuthServiceFactory = ({ } const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, @@ -272,10 +276,14 @@ export const identityJwtAuthServiceFactory = ({ }: TAttachJwtAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); - if (!identityMembershipOrg) { - if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - } + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ message: "Failed to add JWT Auth to already configured identity" @@ -289,14 +297,14 @@ export const identityJwtAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -330,7 +338,7 @@ export const identityJwtAuthServiceFactory = ({ const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { const doc = await identityJwtAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, configurationType, jwksUrl, encryptedJwksCaCert, @@ -349,7 +357,7 @@ export const identityJwtAuthServiceFactory = ({ return doc; }); - return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys }; + return { ...identityJwtAuth, orgId: identityMembershipOrg.scopeOrgId, jwksCaCert, publicKeys }; }; const updateJwtAuth = async ({ @@ -371,7 +379,13 @@ export const identityJwtAuthServiceFactory = ({ actor, actorOrgId }: TUpdateJwtAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { @@ -392,14 +406,14 @@ export const identityJwtAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -462,14 +476,20 @@ export const identityJwtAuthServiceFactory = ({ return { ...updatedJwtAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, jwksCaCert: decryptedJwksCaCert, publicKeys: decryptedPublicKeys }; }; const getJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetJwtAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { @@ -481,7 +501,7 @@ export const identityJwtAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -502,14 +522,20 @@ export const identityJwtAuthServiceFactory = ({ return { ...identityJwtAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, jwksCaCert: decryptedJwksCaCert, publicKeys: decryptedPublicKeys }; }; const revokeJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeJwtAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } @@ -520,10 +546,10 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -532,14 +558,15 @@ export const identityJwtAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -549,7 +576,7 @@ export const identityJwtAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke jwt auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -560,7 +587,7 @@ export const identityJwtAuthServiceFactory = ({ const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx); - return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityJwtAuth; 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 bc231c6d6d..952b1e31de 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 @@ -3,7 +3,7 @@ import axios, { AxiosError } from "axios"; import https from "https"; import RE2 from "re2"; -import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; @@ -28,11 +28,12 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal"; import { extractK8sUsername } from "./identity-kubernetes-auth-fns"; @@ -52,7 +53,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { "create" | "findOne" | "transaction" | "updateById" | "delete" >; identityAccessTokenDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; @@ -60,6 +61,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { gatewayV2Service: TGatewayV2ServiceFactory; gatewayDAL: Pick; gatewayV2DAL: Pick; + orgDAL: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -68,7 +70,7 @@ const GATEWAY_AUTH_DEFAULT_HOST = "https://kubernetes.default.svc.cluster.local" export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, identityAccessTokenDAL, permissionService, licenseService, @@ -76,7 +78,8 @@ export const identityKubernetesAuthServiceFactory = ({ gatewayV2Service, gatewayDAL, gatewayV2DAL, - kmsService + kmsService, + orgDAL }: TIdentityKubernetesAuthServiceFactoryDep) => { const $gatewayProxyWrapper = async ( inputs: { @@ -172,8 +175,9 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityKubernetesAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityKubernetesAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { throw new NotFoundError({ @@ -183,7 +187,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCert = ""; @@ -426,7 +430,7 @@ export const identityKubernetesAuthServiceFactory = ({ } const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, @@ -496,7 +500,13 @@ export const identityKubernetesAuthServiceFactory = ({ }: TAttachKubernetesAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { @@ -512,13 +522,13 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -538,8 +548,8 @@ export const identityKubernetesAuthServiceFactory = ({ let isGatewayV1 = true; if (gatewayId) { - const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` @@ -553,7 +563,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -565,13 +575,13 @@ export const identityKubernetesAuthServiceFactory = ({ const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const doc = await identityKubernetesAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, kubernetesHost, tokenReviewMode, allowedNamespaces, @@ -593,7 +603,7 @@ export const identityKubernetesAuthServiceFactory = ({ return doc; }); - return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; + return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.scopeOrgId }; }; const updateKubernetesAuth = async ({ @@ -615,7 +625,13 @@ export const identityKubernetesAuthServiceFactory = ({ actor, actorOrgId }: TUpdateKubernetesAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { @@ -637,13 +653,13 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -663,8 +679,8 @@ export const identityKubernetesAuthServiceFactory = ({ let isGatewayV1 = true; if (gatewayId) { - const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); if (!gateway && !gatewayV2) { throw new NotFoundError({ @@ -679,7 +695,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -711,7 +727,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); if (caCert !== undefined) { @@ -742,7 +758,7 @@ export const identityKubernetesAuthServiceFactory = ({ return { ...updatedKubernetesAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, caCert: updatedCACert, tokenReviewerJwt: updatedTokenReviewerJwt }; @@ -755,7 +771,13 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetKubernetesAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); @@ -772,7 +794,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -780,7 +802,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCert = ""; @@ -799,7 +821,7 @@ export const identityKubernetesAuthServiceFactory = ({ ...identityKubernetesAuth, caCert, tokenReviewerJwt, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, gatewayId: identityKubernetesAuth.gatewayId ?? identityKubernetesAuth.gatewayV2Id }; }; @@ -811,7 +833,13 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeKubernetesAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { @@ -819,10 +847,10 @@ export const identityKubernetesAuthServiceFactory = ({ message: "The identity does not have kubernetes auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -830,13 +858,14 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -846,7 +875,7 @@ export const identityKubernetesAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke kubernetes auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -856,7 +885,7 @@ export const identityKubernetesAuthServiceFactory = ({ const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.KUBERNETES_AUTH }, tx); - return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityKubernetesAuth; }; diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 46ec2f98a7..1a8ea3ed6f 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -31,11 +31,12 @@ import { logger } from "@app/lib/logger"; 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"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal"; import { @@ -55,7 +56,7 @@ type TIdentityLdapAuthServiceFactoryDep = { TIdentityLdapAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: TKmsServiceFactory; @@ -65,6 +66,7 @@ type TIdentityLdapAuthServiceFactoryDep = { TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" >; + orgDAL: Pick; }; export type TIdentityLdapAuthServiceFactory = ReturnType; @@ -78,18 +80,22 @@ export const identityLdapAuthServiceFactory = ({ identityAccessTokenDAL, identityDAL, identityLdapAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, licenseService, permissionService, kmsService, identityAuthTemplateDAL, - keyStore + keyStore, + orgDAL }: TIdentityLdapAuthServiceFactoryDep) => { const getLdapConfig = async (identityId: string) => { const identity = await identityDAL.findOne({ id: identityId }); if (!identity) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` }); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: identity.id }); + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: identity.id, + scope: AccessScope.Organization + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` }); const ldapAuth = await identityLdapAuthDAL.findOne({ identityId: identity.id }); @@ -101,7 +107,7 @@ export const identityLdapAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityOrgMembership.orgId + orgId: identityOrgMembership.scopeOrgId }); const bindDN = decryptor({ cipherTextBlob: ldapAuth.encryptedBindDN }).toString(); @@ -112,7 +118,7 @@ export const identityLdapAuthServiceFactory = ({ const ldapConfig = { id: ldapAuth.id, - organization: identityOrgMembership.orgId, + organization: identityOrgMembership.scopeOrgId, url: ldapAuth.url, bindDN, bindPass, @@ -144,7 +150,10 @@ export const identityLdapAuthServiceFactory = ({ }; const login = async ({ identityId }: TLoginLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) { throw new UnauthorizedError({ @@ -160,7 +169,7 @@ export const identityLdapAuthServiceFactory = ({ }); } - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); if (!plan.ldap) { throw new BadRequestError({ message: @@ -169,7 +178,7 @@ export const identityLdapAuthServiceFactory = ({ } const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, @@ -237,7 +246,13 @@ export const identityLdapAuthServiceFactory = ({ }: TAttachLdapAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -253,7 +268,7 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -266,7 +281,7 @@ export const identityLdapAuthServiceFactory = ({ ); } - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); if (!plan.ldap) { throw new BadRequestError({ @@ -296,11 +311,11 @@ export const identityLdapAuthServiceFactory = ({ const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => { const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const template = templateId - ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.scopeOrgId) : undefined; let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string }; @@ -354,7 +369,7 @@ export const identityLdapAuthServiceFactory = ({ const doc = await identityLdapAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, encryptedBindDN, encryptedBindPass, searchBase: ldapConfig.searchBase, @@ -376,7 +391,7 @@ export const identityLdapAuthServiceFactory = ({ ); return doc; }); - return { ...identityLdapAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityLdapAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateLdapAuth = async ({ @@ -402,7 +417,13 @@ export const identityLdapAuthServiceFactory = ({ lockoutDurationSeconds, lockoutCounterResetSeconds }: TUpdateLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -423,7 +444,7 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -436,7 +457,7 @@ export const identityLdapAuthServiceFactory = ({ ); } - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); if (!plan.ldap) { throw new BadRequestError({ @@ -465,11 +486,11 @@ export const identityLdapAuthServiceFactory = ({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const template = templateId - ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.scopeOrgId) : undefined; let config: { bindDN?: string; @@ -555,11 +576,17 @@ export const identityLdapAuthServiceFactory = ({ lockoutCounterResetSeconds }); - return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedLdapAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getLdapAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -573,14 +600,14 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const bindDN = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindDN }).toString(); @@ -590,7 +617,7 @@ export const identityLdapAuthServiceFactory = ({ : undefined; ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...ldapIdentityAuth, orgId: identityMembershipOrg.orgId, bindDN, bindPass, ldapCaCertificate }; + return { ...ldapIdentityAuth, orgId: identityMembershipOrg.scopeOrgId, bindDN, bindPass, ldapCaCertificate }; }; const revokeIdentityLdapAuth = async ({ @@ -600,17 +627,23 @@ export const identityLdapAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ message: "The identity does not have LDAP Auth attached" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -618,14 +651,15 @@ export const identityLdapAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -636,7 +670,7 @@ export const identityLdapAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke LDAP auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -647,7 +681,7 @@ export const identityLdapAuthServiceFactory = ({ const [deletedLdapAuth] = await identityLdapAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.LDAP_AUTH }, tx); - return { ...deletedLdapAuth, orgId: identityMembershipOrg.orgId }; + return { ...deletedLdapAuth, orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityLdapAuth; }; @@ -736,7 +770,13 @@ export const identityLdapAuthServiceFactory = ({ actorOrgId, actorAuthMethod }: TClearLdapAuthLockoutsDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -748,7 +788,7 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -758,7 +798,7 @@ export const identityLdapAuthServiceFactory = ({ pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:*` }); - return { deleted, identityId, orgId: identityMembershipOrg.orgId }; + return { deleted, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; return { diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index a4294250c0..bfac3d1589 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; import RE2 from "re2"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -19,9 +19,10 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityOciAuthDALFactory } from "./identity-oci-auth-dal"; import { @@ -36,9 +37,10 @@ import { type TIdentityOciAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; identityOciAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; + orgDAL: Pick; }; export type TIdentityOciAuthServiceFactory = ReturnType; @@ -46,9 +48,10 @@ export type TIdentityOciAuthServiceFactory = ReturnType { const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => { const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); @@ -56,7 +59,10 @@ export const identityOciAuthServiceFactory = ({ throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityOciAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. @@ -92,7 +98,7 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, @@ -154,7 +160,13 @@ export const identityOciAuthServiceFactory = ({ }: TAttachOciAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { @@ -170,13 +182,13 @@ export const identityOciAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -197,7 +209,7 @@ export const identityOciAuthServiceFactory = ({ const identityOciAuth = await identityOciAuthDAL.transaction(async (tx) => { const doc = await identityOciAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type: "iam", tenancyOcid, allowedUsernames, @@ -210,7 +222,7 @@ export const identityOciAuthServiceFactory = ({ ); return doc; }); - return { ...identityOciAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityOciAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateOciAuth = async ({ @@ -226,7 +238,13 @@ export const identityOciAuthServiceFactory = ({ actor, actorOrgId }: TUpdateOciAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { @@ -247,13 +265,13 @@ export const identityOciAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -282,11 +300,17 @@ export const identityOciAuthServiceFactory = ({ : undefined }); - return { ...updatedOciAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedOciAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getOciAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOciAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { @@ -300,12 +324,12 @@ export const identityOciAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...ociIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...ociIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityOciAuth = async ({ @@ -315,17 +339,23 @@ export const identityOciAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeOciAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ message: "The identity does not have OCI auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -333,14 +363,15 @@ export const identityOciAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -351,7 +382,7 @@ export const identityOciAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke OCI auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -362,7 +393,7 @@ export const identityOciAuthServiceFactory = ({ const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx); - return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityOciAuth; }; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 6585e61f33..1218d8e1cd 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -4,7 +4,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -25,11 +25,12 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; @@ -43,22 +44,24 @@ import { type TIdentityOidcAuthServiceFactoryDep = { identityOidcAuthDAL: TIdentityOidcAuthDALFactory; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; + orgDAL: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; export const identityOidcAuthServiceFactory = ({ identityOidcAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, permissionService, licenseService, identityAccessTokenDAL, - kmsService + kmsService, + orgDAL }: TIdentityOidcAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -66,8 +69,9 @@ export const identityOidcAuthServiceFactory = ({ throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityOidcAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityOidcAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { throw new NotFoundError({ @@ -77,7 +81,7 @@ export const identityOidcAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCert = ""; @@ -178,7 +182,7 @@ export const identityOidcAuthServiceFactory = ({ } const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, @@ -245,9 +249,15 @@ export const identityOidcAuthServiceFactory = ({ isActorSuperAdmin }: TAttachOidcAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) { - if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -262,14 +272,14 @@ export const identityOidcAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -289,13 +299,13 @@ export const identityOidcAuthServiceFactory = ({ const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const doc = await identityOidcAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, oidcDiscoveryUrl, encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, boundIssuer, @@ -312,7 +322,7 @@ export const identityOidcAuthServiceFactory = ({ ); return doc; }); - return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; + return { ...identityOidcAuth, orgId: identityMembershipOrg.scopeOrgId, caCert }; }; const updateOidcAuth = async ({ @@ -333,7 +343,13 @@ export const identityOidcAuthServiceFactory = ({ actor, actorOrgId }: TUpdateOidcAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { @@ -354,14 +370,14 @@ export const identityOidcAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -396,7 +412,7 @@ export const identityOidcAuthServiceFactory = ({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); if (caCert !== undefined) { @@ -410,13 +426,19 @@ export const identityOidcAuthServiceFactory = ({ return { ...updatedOidcAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, caCert: updatedCACert }; }; const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { @@ -428,7 +450,7 @@ export const identityOidcAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -438,18 +460,24 @@ export const identityOidcAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const caCert = identityOidcAuth.encryptedCaCertificate ? decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString() : ""; - return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; + return { ...identityOidcAuth, orgId: identityMembershipOrg.scopeOrgId, caCert }; }; const revokeOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeOidcAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } @@ -460,10 +488,10 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -472,14 +500,15 @@ export const identityOidcAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -490,7 +519,7 @@ export const identityOidcAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke oidc auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -501,7 +530,7 @@ export const identityOidcAuthServiceFactory = ({ const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OIDC_AUTH }, tx); - return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityOidcAuth; diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index e5e59607d1..3dba6210da 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, TableName, TIdentities, TIdentityAlicloudAuths, @@ -15,7 +16,7 @@ import { TIdentityUniversalAuths } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types"; @@ -24,96 +25,82 @@ import { buildAuthMethods } from "../identity/identity-fns"; export type TIdentityProjectDALFactory = ReturnType; export const identityProjectDALFactory = (db: TDbClient) => { - const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership); - const findByIdentityId = async (identityId: string, tx?: Knex) => { try { - const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) - .where(`${TableName.IdentityProjectMembership}.identityId`, identityId) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectMembership}.id`, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` - ) - + const docs = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.actorIdentityId`, identityId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.IdentityUniversalAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTokenAuth}.identityId` ) .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity), db.ref("hasDeleteProtection").withSchema(TableName.Identity), - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("projectId").withSchema(TableName.IdentityProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), + db.ref("scopeProjectId").withSchema(TableName.Membership).as("projectId"), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("type").as("projectType").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), @@ -165,7 +152,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { }) }, project: { - id: projectId, + id: projectId as string, name: projectName, type: projectType } @@ -223,12 +210,10 @@ export const identityProjectDALFactory = (db: TDbClient) => { void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`); } }) - .join( - TableName.IdentityProjectMembership, - `${TableName.IdentityProjectMembership}.identityId`, - `${TableName.Identity}.id` - ) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) + .join(TableName.Membership, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .orderBy( `${TableName.Identity}.${filter.orderBy ?? ProjectIdentityOrderBy.Name}`, filter.orderDirection ?? OrderByDirection.ASC @@ -240,33 +225,20 @@ export const identityProjectDALFactory = (db: TDbClient) => { void fetchIdentitySubquery.offset(filter.offset ?? 0).limit(filter.limit); } - const query = (tx || db.replicaNode())(TableName.IdentityProjectMembership) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + const query = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .join(fetchIdentitySubquery, (bd) => { - bd.on(`${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`); + bd.on(`${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`); }) .where((qb) => { if (filter.identityId) { - void qb.where(`${TableName.IdentityProjectMembership}.identityId`, filter.identityId); + void qb.where(`${TableName.Membership}.actorIdentityId`, filter.identityId); } }) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectMembership}.id`, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` - ) - + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.IdentityUniversalAuth, `${TableName.Identity}.id`, @@ -314,23 +286,23 @@ export const identityProjectDALFactory = (db: TDbClient) => { ) .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity), db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity), - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("id").withSchema(TableName.Membership), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -450,13 +422,14 @@ export const identityProjectDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const identities = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) + const identities = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) .where((qb) => { if (filter.identityId) { - void qb.where("identityId", filter.identityId); + void qb.where(`${TableName.Membership}.actorIdentityId`, filter.identityId); } if (filter.search) { @@ -472,7 +445,6 @@ export const identityProjectDALFactory = (db: TDbClient) => { }; return { - ...identityProjectOrm, findByIdentityId, findByProjectId, getCountByProjectId diff --git a/backend/src/services/identity-project/identity-project-membership-role-dal.ts b/backend/src/services/identity-project/identity-project-membership-role-dal.ts deleted file mode 100644 index 3f6c6b589b..0000000000 --- a/backend/src/services/identity-project/identity-project-membership-role-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectMembershipRoleDALFactory = ReturnType; - -export const identityProjectMembershipRoleDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectMembershipRole); - return orm; -}; diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 08fd2cf0be..abe0446da6 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -1,44 +1,22 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; -import { - constructPermissionErrorMessage, - validatePrivilegeChangeOperation -} from "@app/ee/services/permission/permission-fns"; +import { AccessScope, ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; -import { ms } from "@app/lib/ms"; +import { NotFoundError } from "@app/lib/errors"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; -import { TProjectDALFactory } from "../project/project-dal"; -import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TIdentityProjectDALFactory } from "./identity-project-dal"; -import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-membership-role-dal"; import { - TCreateProjectIdentityDTO, - TDeleteProjectIdentityDTO, TGetProjectIdentityByIdentityIdDTO, TGetProjectIdentityByMembershipIdDTO, - TListProjectIdentityDTO, - TUpdateProjectIdentityDTO + TListProjectIdentityDTO } from "./identity-project-types"; type TIdentityProjectServiceFactoryDep = { identityProjectDAL: TIdentityProjectDALFactory; - identityProjectMembershipRoleDAL: Pick< - TIdentityProjectMembershipRoleDALFactory, - "create" | "transaction" | "insertMany" | "delete" - >; - projectDAL: Pick; - projectRoleDAL: Pick; - identityOrgMembershipDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" - >; + permissionService: Pick; + membershipIdentityDAL: TMembershipIdentityDALFactory; }; export type TIdentityProjectServiceFactory = ReturnType; @@ -46,276 +24,8 @@ export type TIdentityProjectServiceFactory = ReturnType { - const createProjectIdentity = async ({ - identityId, - actor, - actorId, - actorOrgId, - actorAuthMethod, - projectId, - roles - }: TCreateProjectIdentityDTO) => { - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Create, - subject(ProjectPermissionSub.Identity, { - identityId - }) - ); - - const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId }); - if (existingIdentity) - throw new BadRequestError({ - message: `Identity with ID ${identityId} already exists in project with ID ${projectId}` - }); - - const project = await projectDAL.findById(projectId); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ - identityId, - orgId: project.orgId - }); - if (!identityOrgMembership) - throw new NotFoundError({ - message: `Failed to find identity with ID ${identityId}` - }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - projectId - ); - - if (requestedRoleChange !== ProjectMembershipRole.NoAccess) { - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) - throw new NotFoundError({ message: "One or more custom project roles not found" }); - - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - const projectIdentity = await identityProjectDAL.transaction(async (tx) => { - const identityProjectMembership = await identityProjectDAL.create( - { - identityId, - projectId: project.id - }, - tx - ); - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: identityProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: identityProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - return { ...identityProjectMembership, roles: identityRoles }; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return projectIdentity; - }; - - const updateProjectIdentity = async ({ - projectId, - identityId, - roles, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TUpdateProjectIdentityDTO) => { - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); - if (!projectIdentity) - throw new NotFoundError({ - message: `Identity with ID ${identityId} doesn't exists in project with ID ${projectId}` - }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - projectId - ); - - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to change role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => - !Object.values(ProjectMembershipRole) - // we don't want to include custom in this check; - // this unintentionally enables setting slug to custom which is reserved - .filter((r) => r !== ProjectMembershipRole.Custom) - .includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) - throw new NotFoundError({ message: "One or more custom project roles not found" }); - - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: projectIdentity.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: projectIdentity.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => { - await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx); - return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return updatedRoles; - }; - - const deleteProjectIdentity = async ({ - identityId, - actorId, - actor, - actorOrgId, - actorAuthMethod, - projectId - }: TDeleteProjectIdentityDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) { - throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Delete, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return deletedIdentity; - }; - const listProjectIdentities = async ({ projectId, actor, @@ -391,9 +101,13 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetProjectIdentityByMembershipIdDTO) => { - const membership = await identityProjectDAL.findOne({ id: identityMembershipId }); + const membership = await membershipIdentityDAL.findOne({ + id: identityMembershipId, + scope: AccessScope.Project, + scopeOrgId: actorOrgId + }); - if (!membership) { + if (!membership || !membership.scopeProjectId || !membership.actorIdentityId) { throw new NotFoundError({ message: `Project membership with ID '${identityMembershipId}' not found` }); @@ -402,7 +116,7 @@ export const identityProjectServiceFactory = ({ const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId: membership.projectId, + projectId: membership.scopeProjectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.Any @@ -410,20 +124,17 @@ export const identityProjectServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: membership.identityId }) + subject(ProjectPermissionSub.Identity, { identityId: membership.actorIdentityId }) ); - const [identityMembership] = await identityProjectDAL.findByProjectId(membership.projectId, { - identityId: membership.identityId + const [identityMembership] = await identityProjectDAL.findByProjectId(membership.scopeProjectId, { + identityId: membership.actorIdentityId }); return identityMembership; }; return { - createProjectIdentity, - updateProjectIdentity, - deleteProjectIdentity, listProjectIdentities, getProjectIdentityByIdentityId, getProjectIdentityByMembershipId diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index ef2463eece..625b9b328c 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -15,11 +15,11 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; @@ -30,7 +30,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = { TIdentityTlsCertAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: Pick; @@ -48,7 +48,7 @@ const parseSubjectDetails = (data: string) => { export const identityTlsCertAuthServiceFactory = ({ identityAccessTokenDAL, identityTlsCertAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, licenseService, permissionService, kmsService @@ -61,8 +61,9 @@ export const identityTlsCertAuthServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityTlsCertAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityTlsCertAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { @@ -73,7 +74,7 @@ export const identityTlsCertAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const caCertificate = decryptor({ @@ -118,7 +119,7 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, @@ -180,7 +181,13 @@ export const identityTlsCertAuthServiceFactory = ({ }) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { @@ -196,13 +203,13 @@ export const identityTlsCertAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -222,13 +229,13 @@ export const identityTlsCertAuthServiceFactory = ({ const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const identityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => { const doc = await identityTlsCertAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, accessTokenMaxTTL, allowedCommonNames, accessTokenTTL, @@ -240,7 +247,7 @@ export const identityTlsCertAuthServiceFactory = ({ ); return doc; }); - return { ...identityTlsCertAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityTlsCertAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateTlsCertAuth: TIdentityTlsCertAuthServiceFactory["updateTlsCertAuth"] = async ({ @@ -256,7 +263,13 @@ export const identityTlsCertAuthServiceFactory = ({ actor, actorOrgId }) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { @@ -278,13 +291,13 @@ export const identityTlsCertAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -303,7 +316,7 @@ export const identityTlsCertAuthServiceFactory = ({ }); const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const updatedTlsCertAuth = await identityTlsCertAuthDAL.updateById(identityTlsCertAuth.id, { @@ -319,7 +332,7 @@ export const identityTlsCertAuthServiceFactory = ({ : undefined }); - return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getTlsCertAuth: TIdentityTlsCertAuthServiceFactory["getTlsCertAuth"] = async ({ @@ -329,7 +342,13 @@ export const identityTlsCertAuthServiceFactory = ({ actorAuthMethod, actorOrgId }) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { @@ -343,21 +362,21 @@ export const identityTlsCertAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCertificate = ""; if (identityAuth.encryptedCaCertificate) { caCertificate = decryptor({ cipherTextBlob: identityAuth.encryptedCaCertificate }).toString(); } - return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.orgId }; + return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeTlsCertAuth: TIdentityTlsCertAuthServiceFactory["revokeTlsCertAuth"] = async ({ @@ -367,32 +386,39 @@ export const identityTlsCertAuthServiceFactory = ({ actorAuthMethod, actorOrgId }) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ message: "The identity does not have TLS Certificate auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, memberships } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const shouldUseNewPrivilegeSystem = Boolean(memberships?.[0]?.shouldUseNewPrivilegeSystem); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -403,7 +429,7 @@ export const identityTlsCertAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke TLS Certificate auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -414,7 +440,7 @@ export const identityTlsCertAuthServiceFactory = ({ const deletedTlsCertAuth = await identityTlsCertAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.TLS_CERT_AUTH }, tx); - return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityTlsCertAuth; }; diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts index b7a08276b7..eb9f4ab5dd 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts @@ -1,4 +1,4 @@ -import { TIdentityAccessTokens, TIdentityOrgMemberships, TIdentityTlsCertAuths } from "@app/db/schemas"; +import { TIdentityAccessTokens, TIdentityTlsCertAuths, TMemberships } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TLoginTlsCertAuthDTO = { @@ -40,7 +40,7 @@ export type TIdentityTlsCertAuthServiceFactory = { identityTlsCertAuth: TIdentityTlsCertAuths; accessToken: string; identityAccessToken: TIdentityAccessTokens; - identityMembershipOrg: TIdentityOrgMemberships; + identityMembershipOrg: TMemberships; }>; attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise; updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index d3743bd96f..2ae05cb972 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod, TableName } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -14,9 +14,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/li import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal"; import { @@ -35,13 +36,14 @@ type TIdentityTokenAuthServiceFactoryDep = { TIdentityTokenAuthDALFactory, "transaction" | "create" | "findOne" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick< TIdentityAccessTokenDALFactory, "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" >; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; }; export type TIdentityTokenAuthServiceFactory = ReturnType; @@ -49,10 +51,11 @@ export type TIdentityTokenAuthServiceFactory = ReturnType { const attachTokenAuth = async ({ identityId, @@ -68,7 +71,13 @@ export const identityTokenAuthServiceFactory = ({ }: TAttachTokenAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -84,13 +93,13 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -111,7 +120,7 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const doc = await identityTokenAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, @@ -121,7 +130,7 @@ export const identityTokenAuthServiceFactory = ({ ); return doc; }); - return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateTokenAuth = async ({ @@ -138,7 +147,13 @@ export const identityTokenAuthServiceFactory = ({ }: TUpdateTokenAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -160,13 +175,13 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -195,12 +210,18 @@ export const identityTokenAuthServiceFactory = ({ return { ...updatedTokenAuth, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -214,13 +235,13 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityTokenAuth = async ({ @@ -233,7 +254,13 @@ export const identityTokenAuthServiceFactory = ({ }: TRevokeTokenAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -244,22 +271,23 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -269,7 +297,7 @@ export const identityTokenAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke token auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -283,7 +311,7 @@ export const identityTokenAuthServiceFactory = ({ authMethod: IdentityAuthMethod.TOKEN_AUTH }); - return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityTokenAuth; }; @@ -299,7 +327,13 @@ export const identityTokenAuthServiceFactory = ({ }: TCreateTokenAuthTokenDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -310,22 +344,23 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -335,7 +370,7 @@ export const identityTokenAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create token for identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity ), @@ -345,7 +380,7 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, @@ -400,7 +435,13 @@ export const identityTokenAuthServiceFactory = ({ }: TGetTokenAuthTokensDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -411,7 +452,7 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -443,7 +484,13 @@ export const identityTokenAuthServiceFactory = ({ }); if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId: foundToken.identityId + }); if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); } @@ -457,21 +504,22 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -481,7 +529,7 @@ export const identityTokenAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update token for identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity ), @@ -523,8 +571,9 @@ export const identityTokenAuthServiceFactory = ({ await validateIdentityUpdateForSuperAdminPrivileges(identityAccessToken.identityId, isActorSuperAdmin); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ - identityId: identityAccessToken.identityId + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAccessToken.identityId, + scope: AccessScope.Organization }); if (!identityOrgMembership) { @@ -534,7 +583,7 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 979e6f25fb..563a3f897d 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -22,9 +22,10 @@ import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal"; import { TIdentityUaDALFactory } from "./identity-ua-dal"; @@ -44,9 +45,10 @@ type TIdentityUaServiceFactoryDep = { identityUaDAL: TIdentityUaDALFactory; identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; - identityOrgMembershipDAL: TIdentityOrgDALFactory; + membershipIdentityDAL: TMembershipIdentityDALFactory; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; keyStore: Pick< TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" @@ -64,9 +66,10 @@ export const identityUaServiceFactory = ({ identityUaDAL, identityUaClientSecretDAL, identityAccessTokenDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, permissionService, licenseService, + orgDAL, keyStore }: TIdentityUaServiceFactoryDep) => { const login = async (clientId: string, clientSecret: string, ip: string) => { @@ -97,7 +100,10 @@ export const identityUaServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityUa.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) { throw new UnauthorizedError({ message: "Invalid credentials" @@ -223,7 +229,7 @@ export const identityUaServiceFactory = ({ const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, @@ -295,7 +301,13 @@ export const identityUaServiceFactory = ({ }: TAttachUaDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -311,13 +323,13 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { if ( !plan.ipAllowlisting && @@ -354,7 +366,7 @@ export const identityUaServiceFactory = ({ const identityUa = await identityUaDAL.transaction(async (tx) => { const doc = await identityUaDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, clientId: crypto.nativeCrypto.randomUUID(), clientSecretTrustedIps: JSON.stringify(reformattedClientSecretTrustedIps), accessTokenMaxTTL, @@ -371,7 +383,7 @@ export const identityUaServiceFactory = ({ ); return doc; }); - return { ...identityUa, orgId: identityMembershipOrg.orgId }; + return { ...identityUa, orgId: identityMembershipOrg.scopeOrgId }; }; const updateUniversalAuth = async ({ @@ -391,7 +403,13 @@ export const identityUaServiceFactory = ({ lockoutDurationSeconds, lockoutCounterResetSeconds }: TUpdateUaDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); @@ -415,13 +433,13 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => { if ( !plan.ipAllowlisting && @@ -471,11 +489,17 @@ export const identityUaServiceFactory = ({ lockoutDurationSeconds, lockoutCounterResetSeconds }); - return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedUaAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); @@ -492,12 +516,12 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...uaIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityUniversalAuth = async ({ @@ -507,7 +531,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeUaDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -518,21 +548,22 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -542,7 +573,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke universal auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -551,7 +582,7 @@ export const identityUaServiceFactory = ({ const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx); - return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityUniversalAuth; }; @@ -566,7 +597,13 @@ export const identityUaServiceFactory = ({ description, numUsesLimit }: TCreateUaClientSecretDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -575,10 +612,10 @@ export const identityUaServiceFactory = ({ }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -586,13 +623,14 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -602,7 +640,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create client secret for identity.", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity ), @@ -613,7 +651,7 @@ export const identityUaServiceFactory = ({ const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecretHash = await crypto.hashing().createHash(clientSecret, appCfg.SALT_ROUNDS); - const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identityId }); + const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identity.id }); if (!identityUaAuth) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const identityUaClientSecret = await identityUaClientSecretDAL.create({ @@ -629,7 +667,7 @@ export const identityUaServiceFactory = ({ return { clientSecret, clientSecretData: identityUaClientSecret, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; @@ -640,7 +678,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, identityId }: TGetUaClientSecretsDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -648,10 +692,10 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -659,14 +703,15 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity, permission, @@ -676,7 +721,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to get identity client secret with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity ), @@ -691,7 +736,7 @@ export const identityUaServiceFactory = ({ identityUAId: identityUniversalAuth.id, isClientSecretRevoked: false }); - return { clientSecrets, orgId: identityMembershipOrg.orgId }; + return { clientSecrets, orgId: identityMembershipOrg.scopeOrgId }; }; const getUniversalAuthClientSecretById = async ({ @@ -702,7 +747,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, clientSecretId }: TGetUniversalAuthClientSecretByIdDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -717,10 +768,10 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -728,13 +779,14 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity, permission, @@ -744,14 +796,14 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to read identity client secret of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } }); - return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; + return { ...clientSecret, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeUniversalAuthClientSecret = async ({ @@ -762,7 +814,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, clientSecretId }: TRevokeUaClientSecretDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -777,10 +835,10 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -788,14 +846,15 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity, permission, @@ -805,7 +864,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke identity client secret with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity ), @@ -817,7 +876,7 @@ export const identityUaServiceFactory = ({ isClientSecretRevoked: true }); - return { ...updatedClientSecret, identityId, orgId: identityMembershipOrg.orgId }; + return { ...updatedClientSecret, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; const clearUniversalAuthLockouts = async ({ @@ -827,7 +886,13 @@ export const identityUaServiceFactory = ({ actorOrgId, actorAuthMethod }: TClearUaLockoutsDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -839,7 +904,7 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -849,7 +914,7 @@ export const identityUaServiceFactory = ({ pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:*` }); - return { deleted, identityId, orgId: identityMembershipOrg.orgId }; + return { deleted, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; return { diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index 7bc7976004..a2e4995ca4 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -50,11 +50,23 @@ export const identityDALFactory = (db: TDbClient) => { }); } + const countQuery = query.clone(); + if (sortBy) { query = query.orderBy(sortBy); } - return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)); + const [identities, totalResult] = await Promise.all([ + query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)), + countQuery.countDistinct(`${TableName.Identity}.id`, { as: "count" }).first() + ]); + + const total = Number(totalResult?.count || 0); + + return { + identities, + total + }; } catch (error) { throw new DatabaseError({ error, name: "Get identities by filter" }); } diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index c083df5aa0..65aee561cd 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, TableName, TIdentityAlicloudAuths, TIdentityAwsAuths, @@ -11,15 +12,15 @@ import { TIdentityKubernetesAuths, TIdentityOciAuths, TIdentityOidcAuths, - TIdentityOrgMemberships, TIdentityTlsCertAuths, TIdentityTokenAuths, TIdentityUniversalAuths, - TOrgRoles + TMembershipRoles, + TMemberships } from "@app/db/schemas"; import { TIdentityLdapAuths } from "@app/db/schemas/identity-ldap-auths"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; import { OrderByDirection } from "@app/lib/types"; import { @@ -33,80 +34,80 @@ import { buildAuthMethods } from "./identity-fns"; export type TIdentityOrgDALFactory = ReturnType; export const identityOrgDALFactory = (db: TDbClient) => { - const identityOrgOrm = ormify(db, TableName.IdentityOrgMembership); - - const findOne = async (filter: Partial, tx?: Knex) => { + const findOne = async (filter: Partial, tx?: Knex) => { try { - const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) + const [data] = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where((queryBuilder) => { Object.entries(filter).forEach(([key, value]) => { - void queryBuilder.where(`${TableName.IdentityOrgMembership}.${key}`, value); + void queryBuilder.where(`${TableName.Membership}.${key}`, value); }); }) - .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) .leftJoin( TableName.IdentityUniversalAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTokenAuth}.identityId` ) .leftJoin( TableName.IdentityJwtAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityJwtAuth}.identityId` ) .leftJoin( TableName.IdentityLdapAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityLdapAuth}.identityId` ) .leftJoin( TableName.IdentityTlsCertAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTlsCertAuth}.identityId` ) .select( - selectAllTableCols(TableName.IdentityOrgMembership), + selectAllTableCols(TableName.Membership), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -129,7 +130,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { return { ...data, identity: { - id: data.identityId, + id: data.actorIdentityId as string, name, hasDeleteProtection, authMethods: buildAuthMethods(data) @@ -149,20 +150,18 @@ export const identityOrgDALFactory = (db: TDbClient) => { orderDirection = OrderByDirection.ASC, search, ...filter - }: Partial & + }: Partial & Pick, tx?: Knex ) => { try { const paginatedIdentity = (tx || db.replicaNode())(TableName.Identity) - .join( - TableName.IdentityOrgMembership, - `${TableName.IdentityOrgMembership}.identityId`, - `${TableName.Identity}.id` - ) + .join(TableName.Membership, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .select( - selectAllTableCols(TableName.IdentityOrgMembership), + selectAllTableCols(TableName.Membership), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity) ) @@ -181,84 +180,87 @@ export const identityOrgDALFactory = (db: TDbClient) => { type TSubquery = Awaited; const query = (tx || db.replicaNode()) .from(paginatedIdentity) - .leftJoin(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`) - + .join( + TableName.MembershipRole, + `${TableName.MembershipRole}.membershipId`, + "paginatedIdentity.id" + ) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`paginatedIdentity.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`paginatedIdentity.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) - .leftJoin( TableName.IdentityUniversalAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityTokenAuth}.identityId` ) .leftJoin( TableName.IdentityJwtAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityJwtAuth}.identityId` ) .leftJoin( TableName.IdentityLdapAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityLdapAuth}.identityId` ) .leftJoin( TableName.IdentityTlsCertAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityTlsCertAuth}.identityId` ) .select( db.ref("id").withSchema("paginatedIdentity"), - db.ref("role").withSchema("paginatedIdentity"), - db.ref("roleId").withSchema("paginatedIdentity"), - db.ref("orgId").withSchema("paginatedIdentity"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("scopeOrgId").withSchema("paginatedIdentity").as("orgId"), db.ref("lastLoginAuthMethod").withSchema("paginatedIdentity"), db.ref("lastLoginTime").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"), - db.ref("identityId").withSchema("paginatedIdentity").as("identityId"), + db.ref("actorIdentityId").withSchema("paginatedIdentity").as("identityId"), db.ref("identityName").withSchema("paginatedIdentity"), db.ref("hasDeleteProtection").withSchema("paginatedIdentity"), @@ -276,12 +278,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth) ) // cr stands for custom role - .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) - .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) - .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles)) - .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) + .select(db.ref("id").as("crId").withSchema(TableName.Role)) + .select(db.ref("name").as("crName").withSchema(TableName.Role)) + .select(db.ref("slug").as("crSlug").withSchema(TableName.Role)) + .select(db.ref("description").as("crDescription").withSchema(TableName.Role)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.Role)) .select( db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), @@ -327,7 +328,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { }) => ({ role, roleId, - identityId, + identityId: identityId as string, id, orgId, createdAt, @@ -344,7 +345,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { } : undefined, identity: { - id: identityId, + id: identityId as string, name: identityName, hasDeleteProtection, authMethods: buildAuthMethods({ @@ -394,20 +395,23 @@ export const identityOrgDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const searchQuery = (tx || db.replicaNode())(TableName.IdentityOrgMembership) - .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`) - .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) - .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + const searchQuery = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .orderBy( orderBy === OrgIdentityOrderBy.Role - ? `${TableName.IdentityOrgMembership}.${orderBy}` + ? `${TableName.MembershipRole}.${orderBy}` : `${TableName.Identity}.${orderBy}`, orderDirection ) - .select(`${TableName.IdentityOrgMembership}.id`) + .select(`${TableName.Membership}.id`) .select<{ id: string; total_count: string }>( db.raw( - `count(${TableName.IdentityOrgMembership}."identityId") OVER(PARTITION BY ${TableName.IdentityOrgMembership}."orgId") as total_count` + `count(${TableName.Membership}."actorIdentityId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total_count` ) ) .as("searchedIdentities"); @@ -416,7 +420,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { buildKnexFilterForSearchResource(searchQuery, searchFilter, (attr) => { switch (attr) { case "role": - return [`${TableName.OrgRoles}.slug`, `${TableName.IdentityOrgMembership}.role`]; + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; case "name": return `${TableName.Identity}.name`; default: @@ -430,82 +434,85 @@ export const identityOrgDALFactory = (db: TDbClient) => { } type TSubquery = Awaited; - const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) - .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) - .join(searchQuery, `${TableName.IdentityOrgMembership}.id`, "searchedIdentities.id") - .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) - .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + const query = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .join(searchQuery, `${TableName.Membership}.id`, "searchedIdentities.id") + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) .leftJoin( TableName.IdentityUniversalAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTokenAuth}.identityId` ) .leftJoin( TableName.IdentityJwtAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityJwtAuth}.identityId` ) .leftJoin( TableName.IdentityLdapAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityLdapAuth}.identityId` ) .select( - db.ref("id").withSchema(TableName.IdentityOrgMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("total_count").withSchema("searchedIdentities"), - db.ref("role").withSchema(TableName.IdentityOrgMembership), - db.ref("roleId").withSchema(TableName.IdentityOrgMembership), - db.ref("orgId").withSchema(TableName.IdentityOrgMembership), - db.ref("createdAt").withSchema(TableName.IdentityOrgMembership), - db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership), - db.ref("lastLoginAuthMethod").withSchema(TableName.IdentityOrgMembership), - db.ref("lastLoginTime").withSchema(TableName.IdentityOrgMembership), - db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), + db.ref("lastLoginAuthMethod").withSchema(TableName.Membership), + db.ref("lastLoginTime").withSchema(TableName.Membership), + db.ref("actorIdentityId").withSchema(TableName.Membership).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity), @@ -522,12 +529,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth) ) // cr stands for custom role - .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) - .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) - .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles)) - .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) + .select(db.ref("id").as("crId").withSchema(TableName.Role)) + .select(db.ref("name").as("crName").withSchema(TableName.Role)) + .select(db.ref("slug").as("crSlug").withSchema(TableName.Role)) + .select(db.ref("description").as("crDescription").withSchema(TableName.Role)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.Role)) .select( db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), @@ -545,13 +551,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { ELSE ??.role END ? `, - [ - TableName.IdentityOrgMembership, - "custom", - TableName.OrgRoles, - TableName.IdentityOrgMembership, - db.raw(orderDirection) - ] + [TableName.MembershipRole, "custom", TableName.Role, TableName.MembershipRole, db.raw(orderDirection)] ); } @@ -590,7 +590,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { }) => ({ role, roleId, - identityId, + identityId: identityId as string, id, total_count: total_count as string, orgId, @@ -608,7 +608,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { } : undefined, identity: { - id: identityId, + id: identityId as string, name: identityName, hasDeleteProtection, authMethods: buildAuthMethods({ @@ -646,13 +646,15 @@ export const identityOrgDALFactory = (db: TDbClient) => { }; const countAllOrgIdentities = async ( - { search, ...filter }: Partial & Pick, + { search, ...filter }: Partial & Pick, tx?: Knex ) => { try { - const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) + const query = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where(filter) - .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) .count(); if (search?.length) { @@ -667,5 +669,5 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; - return { ...identityOrgOrm, find, findOne, countAllOrgIdentities, searchIdentities }; + return { find, findOne, countAllOrgIdentities, searchIdentities }; }; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index f216d64836..721844070c 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, TableName, TRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -12,6 +12,9 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityMetadataDALFactory } from "./identity-metadata-dal"; @@ -30,10 +33,13 @@ type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityMetadataDAL: TIdentityMetadataDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; + membershipIdentityDAL: TMembershipIdentityDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; identityProjectDAL: Pick; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; keyStore: Pick; + orgDAL: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -45,7 +51,10 @@ export const identityServiceFactory = ({ identityProjectDAL, permissionService, licenseService, - keyStore + keyStore, + orgDAL, + membershipIdentityDAL, + membershipRoleDAL }: TIdentityServiceFactoryDep) => { const createIdentity = async ({ name, @@ -58,33 +67,26 @@ export const identityServiceFactory = ({ actorOrgId, metadata }: TCreateIdentityDTO) => { - const { permission, membership } = await permissionService.getOrgPermission( - actor, - actorId, - orgId, - actorAuthMethod, - actorOrgId - ); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( - role, - orgId - ); - const isCustomRole = Boolean(customRole); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], orgId); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); if (role !== OrgMembershipRole.NoAccess) { const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create identity", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity ), @@ -103,12 +105,20 @@ export const identityServiceFactory = ({ const identity = await identityDAL.transaction(async (tx) => { const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx); - await identityOrgMembershipDAL.create( + const membership = await membershipIdentityDAL.create( { - identityId: newIdentity.id, - orgId, + scope: AccessScope.Organization, + actorIdentityId: newIdentity.id, + scopeOrgId: orgId + }, + tx + ); + + await membershipRoleDAL.create( + { + membershipId: membership.id, role: isCustomRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + customRoleId: rolePermissionDetails?.role?.id }, tx ); @@ -155,45 +165,47 @@ export const identityServiceFactory = ({ }: TUpdateIdentityDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: id, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - let customRole: TOrgRoles | undefined; + let customRole: TRoles | undefined; if (role) { - const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole( - role, - identityOrgMembership.orgId - ); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); - const isCustomRole = Boolean(customOrgRole); + const isCustomRole = Boolean(rolePermissionDetails?.role); const appliedRolePermissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity, permission, - rolePermission + rolePermissionDetails?.permission ); if (!appliedRolePermissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update identity", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity ), details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } }); - if (isCustomRole) customRole = customOrgRole; + if (isCustomRole) customRole = rolePermissionDetails?.role; } const identity = await identityDAL.transaction(async (tx) => { @@ -203,11 +215,12 @@ export const identityServiceFactory = ({ : await identityDAL.findById(id, tx); if (role) { - await identityOrgMembershipDAL.updateById( - identityOrgMembership.id, + await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx); + await membershipRoleDAL.create( { + membershipId: identityOrgMembership.id, role: customRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id || null + customRoleId: customRole?.id || null }, tx ); @@ -219,12 +232,12 @@ export const identityServiceFactory = ({ }> = []; if (metadata) { - await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx); + await identityMetadataDAL.delete({ orgId: identityOrgMembership.scopeOrgId, identityId: id }, tx); if (metadata.length) { const rowsToInsert = metadata.map(({ key, value }) => ({ identityId: newIdentity.id, - orgId: identityOrgMembership.orgId, + orgId: identityOrgMembership.scopeOrgId, key, value })); @@ -239,12 +252,14 @@ export const identityServiceFactory = ({ }; }); - return { ...identity, orgId: identityOrgMembership.orgId }; + return { ...identity, orgId: identityOrgMembership.scopeOrgId }; }; const getIdentityById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetIdentityByIdDTO) => { const doc = await identityOrgMembershipDAL.find({ - [`${TableName.IdentityOrgMembership}.identityId` as "identityId"]: id + [`${TableName.Membership}.actorIdentityId` as "actorIdentityId"]: id, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId }); const identity = doc[0]; if (!identity) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); @@ -258,6 +273,7 @@ export const identityServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + // TODO(namespace): check this in identity service const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); const activeLockoutAuthMethods = new Set(); @@ -289,14 +305,19 @@ export const identityServiceFactory = ({ isActorSuperAdmin }: TDeleteIdentityDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); - - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); + const identityOrgMembership = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId: id + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -308,9 +329,9 @@ export const identityServiceFactory = ({ const deletedIdentity = await identityDAL.deleteById(id); - await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId); + await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); - return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; + return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; }; const listOrgIdentities = async ({ @@ -329,7 +350,8 @@ export const identityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ - [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + scope: AccessScope.Organization, limit, offset, orderBy, @@ -338,7 +360,7 @@ export const identityServiceFactory = ({ }); const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({ - [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, search }); @@ -379,13 +401,17 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId }: TListProjectIdentitiesByIdentityIdDTO) => { - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId }); + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: identityId, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 995919c55e..4de44a3458 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -392,7 +392,7 @@ export const kmsServiceFactory = ({ }; const importKeyMaterial = async ( - { key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO, + { key, algorithm, name, isReserved, projectId, orgId, keyUsage, kmipMetadata }: TImportKeyMaterialDTO, tx?: Knex ) => { // daniel: currently we only support imports for encrypt/decrypt keys @@ -416,7 +416,8 @@ export const kmsServiceFactory = ({ keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT, orgId, isReserved, - projectId + projectId, + kmipMetadata }, db ); diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index ca2401bb61..eaf0adbdec 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -99,4 +99,5 @@ export type TImportKeyMaterialDTO = { projectId: string; orgId: string; keyUsage: KmsKeyUsage; + kmipMetadata?: Record; }; diff --git a/backend/src/services/membership-group/membership-group-dal.ts b/backend/src/services/membership-group/membership-group-dal.ts new file mode 100644 index 0000000000..b3c35e46f3 --- /dev/null +++ b/backend/src/services/membership-group/membership-group-dal.ts @@ -0,0 +1,272 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessScope, AccessScopeData, MembershipsSchema, TableName } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; +import { TSearchResourceOperator } from "@app/lib/search-resource/search"; + +export type TMembershipGroupDALFactory = ReturnType; + +type TFindGroupArg = { + scopeData: AccessScopeData; + tx?: Knex; + filter: Partial<{ + limit: number; + offset: number; + groupId: string; + name: Omit; + role: Omit; + }>; +}; + +type TGetGroupByIdArg = { + scopeData: AccessScopeData; + tx?: Knex; + groupId: string; +}; + +export const membershipGroupDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + + const getGroupById = async ({ scopeData, tx, groupId }: TGetGroupByIdArg) => { + try { + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Membership}.actorGroupId`, groupId) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("name").withSchema(TableName.Role).as("roleName"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt") + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { groupName, groupSlug } = el; + return { + ...MembershipsSchema.parse(el), + group: { + id: groupId, + name: groupName, + slug: groupSlug + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + roleName, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + customRoleName: roleName, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipGetByGroupId" }); + } + }; + + const findGroups = async ({ scopeData, tx, filter }: TFindGroupArg) => { + try { + const paginatedGroups = (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where((qb) => { + if (filter.groupId) { + void qb.where(`${TableName.Groups}.id`, filter.groupId); + } + + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }); + + if (filter.limit) void paginatedGroups.limit(filter.limit); + if (filter.offset) void paginatedGroups.offset(filter.offset); + + if (filter.name || filter.role) { + buildKnexFilterForSearchResource( + paginatedGroups, + { + name: filter.name!, + role: filter.role! + }, + (attr) => { + switch (attr) { + case "role": + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; + case "name": + return `${TableName.Groups}.name`; + default: + throw new BadRequestError({ message: `Invalid ${String(attr)} provided` }); + } + } + ); + } + + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .whereIn(`${TableName.Membership}.id`, paginatedGroups) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), + db.ref("id").withSchema(TableName.Groups).as("groupId"), + + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("name").withSchema(TableName.Role).as("roleName"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt") + ) + .select( + db.raw( + `count(${TableName.Membership}."actorGroupId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total` + ) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { groupId, groupName, groupSlug } = el; + return { + ...MembershipsSchema.parse(el), + group: { + id: groupId, + name: groupName, + slug: groupSlug + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + roleName, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + customRoleName: roleName, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + return { data, totalCount: Number((data?.[0] as unknown as { total: number })?.total ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipfindGroup" }); + } + }; + + return { ...orm, findGroups, getGroupById }; +}; diff --git a/backend/src/services/membership-group/membership-group-service.ts b/backend/src/services/membership-group/membership-group-service.ts new file mode 100644 index 0000000000..18f6b3ad11 --- /dev/null +++ b/backend/src/services/membership-group/membership-group-service.ts @@ -0,0 +1,325 @@ +import { AccessScope, ProjectMembershipRole, TemporaryPermissionMode, TMembershipRolesInsert } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; +import { SearchResourceOperators } from "@app/lib/search-resource/search"; + +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TRoleDALFactory } from "../role/role-dal"; +import { TMembershipGroupDALFactory } from "./membership-group-dal"; +import { + TCreateMembershipGroupDTO, + TDeleteMembershipGroupDTO, + TGetMembershipGroupByGroupIdDTO, + TListMembershipGroupDTO, + TUpdateMembershipGroupDTO +} from "./membership-group-types"; +import { newNamespaceMembershipGroupFactory } from "./namespace/namespace-membership-group-factory"; +import { newOrgMembershipGroupFactory } from "./org/org-membership-group-factory"; +import { newProjectMembershipGroupFactory } from "./project/project-membership-group-factory"; + +type TMembershipGroupServiceFactoryDep = { + membershipGroupDAL: TMembershipGroupDALFactory; + membershipRoleDAL: Pick; + roleDAL: Pick; + permissionService: TPermissionServiceFactory; + orgDAL: TOrgDALFactory; +}; + +export type TMembershipGroupServiceFactory = ReturnType; + +export const membershipGroupServiceFactory = ({ + membershipGroupDAL, + roleDAL, + membershipRoleDAL, + orgDAL, + permissionService +}: TMembershipGroupServiceFactoryDep) => { + const scopeFactory = { + [AccessScope.Organization]: newOrgMembershipGroupFactory({ + orgDAL, + permissionService + }), + [AccessScope.Namespace]: newNamespaceMembershipGroupFactory({}), + [AccessScope.Project]: newProjectMembershipGroupFactory({ + membershipGroupDAL, + orgDAL, + permissionService + }) + }; + + const createMembership = async (dto: TCreateMembershipGroupDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Group must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + await factory.onCreateMembershipGroupGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membership = await membershipGroupDAL.transaction(async (tx) => { + const doc = await membershipGroupDAL.create( + { + scope: scopeData.scope, + ...scopeDatabaseFields, + actorGroupId: dto.data.groupId + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.insertMany(roleDocs, tx); + return doc; + }); + + return { membership }; + }; + + const updateMembership = async (dto: TUpdateMembershipGroupDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onUpdateMembershipGroupGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Group must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipGroupDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorGroupId: dto.selector.groupId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Group doesn't have membership" + }); + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipGroupDAL.transaction(async (tx) => { + const doc = + typeof data?.isActive === "undefined" + ? existingMembership + : await membershipGroupDAL.updateById( + existingMembership.id, + { + isActive: data.isActive + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.delete( + { + membershipId: doc.id + }, + tx + ); + const roles = await membershipRoleDAL.insertMany(roleDocs, tx); + return { ...doc, roles }; + }); + + return { membership: membershipDoc }; + }; + + const deleteMembership = async (dto: TDeleteMembershipGroupDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onDeleteMembershipGroupGuard(dto); + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipGroupDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorGroupId: dto.selector.groupId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Group doesn't have membership" + }); + + if (existingMembership.actorGroupId === dto.permission.id) + throw new BadRequestError({ + message: "You can't delete your own membership" + }); + + const membershipDoc = await membershipGroupDAL.transaction(async (tx) => { + await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx); + const doc = await membershipGroupDAL.deleteById(existingMembership.id, tx); + return doc; + }); + return { membership: membershipDoc }; + }; + + const listMemberships = async (dto: TListMembershipGroupDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipGroupGuard(dto); + const memberships = await membershipGroupDAL.findGroups({ + scopeData, + filter: { + limit: dto.data.limit, + offset: dto.data.offset, + name: dto.data.groupName + ? { + [SearchResourceOperators.$contains]: dto.data.groupName + } + : undefined, + role: dto.data.roles?.length + ? { + [SearchResourceOperators.$in]: dto.data.roles + } + : undefined + } + }); + return { memberships: memberships.data, totalCount: memberships.totalCount }; + }; + + const getMembershipByGroupId = async (dto: TGetMembershipGroupByGroupIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetMembershipGroupByGroupIdGuard(dto); + const membership = await membershipGroupDAL.getGroupById({ + scopeData, + groupId: selector.groupId + }); + if (!membership) throw new NotFoundError({ message: `Group membership not found` }); + + return { membership }; + }; + + return { + createMembership, + updateMembership, + deleteMembership, + listMemberships, + getMembershipByGroupId + }; +}; diff --git a/backend/src/services/membership-group/membership-group-types.ts b/backend/src/services/membership-group/membership-group-types.ts new file mode 100644 index 0000000000..19c374458d --- /dev/null +++ b/backend/src/services/membership-group/membership-group-types.ts @@ -0,0 +1,79 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TMembershipGroupScopeFactory { + onCreateMembershipGroupGuard: (arg: TCreateMembershipGroupDTO) => Promise; + + onUpdateMembershipGroupGuard: (arg: TUpdateMembershipGroupDTO) => Promise; + onDeleteMembershipGroupGuard: (arg: TDeleteMembershipGroupDTO) => Promise; + onListMembershipGroupGuard: (arg: TListMembershipGroupDTO) => Promise; + onGetMembershipGroupByGroupIdGuard: (arg: TGetMembershipGroupByGroupIdDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + getScopeDatabaseFields: (scope: AccessScopeData) => { + scopeOrgId: string; + scopeNamespaceId?: string | null; + scopeProjectId?: string | null; + }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + groupId: string; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TUpdateMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + groupId: string; + }; + data: { + isActive?: boolean; + metadata?: { key: string; value: string }[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TListMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + limit?: number; + offset?: number; + groupName?: string; + roles?: string[]; + }; +}; + +export type TDeleteMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + groupId: string; + }; +}; + +export type TGetMembershipGroupByGroupIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + groupId: string; + }; +}; diff --git a/backend/src/services/membership-group/namespace/namespace-membership-group-factory.ts b/backend/src/services/membership-group/namespace/namespace-membership-group-factory.ts new file mode 100644 index 0000000000..5c8973b1b5 --- /dev/null +++ b/backend/src/services/membership-group/namespace/namespace-membership-group-factory.ts @@ -0,0 +1,61 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TMembershipGroupScopeFactory } from "../membership-group-types"; + +type TNamespaceMembershipGroupScopeFactoryDep = Record; + +export const newNamespaceMembershipGroupFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceMembershipGroupScopeFactoryDep +): TMembershipGroupScopeFactory => { + const getScopeField: TMembershipGroupScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const getScopeDatabaseFields: TMembershipGroupScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { scopeOrgId: dto.orgId, scopeNamespaceId: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const isCustomRole: TMembershipGroupScopeFactory["isCustomRole"] = () => { + throw new InternalServerError({ message: "Namespace membership group isCustomRole not implemented" }); + }; + + const onCreateMembershipGroupGuard: TMembershipGroupScopeFactory["onCreateMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group create not implemented" }); + }; + + const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group update not implemented" }); + }; + + const onDeleteMembershipGroupGuard: TMembershipGroupScopeFactory["onDeleteMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group delete not implemented" }); + }; + + const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group list not implemented" }); + }; + + const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership group get by group id not implemented" }); + }; + + return { + onCreateMembershipGroupGuard, + onUpdateMembershipGroupGuard, + onDeleteMembershipGroupGuard, + onListMembershipGroupGuard, + onGetMembershipGroupByGroupIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-group/org/org-membership-group-factory.ts b/backend/src/services/membership-group/org/org-membership-group-factory.ts new file mode 100644 index 0000000000..1e87ee3ca1 --- /dev/null +++ b/backend/src/services/membership-group/org/org-membership-group-factory.ts @@ -0,0 +1,125 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; + +import { TMembershipGroupScopeFactory } from "../membership-group-types"; + +type TOrgMembershipGroupScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; +}; + +export const newOrgMembershipGroupFactory = ({ + permissionService, + orgDAL +}: TOrgMembershipGroupScopeFactoryDep): TMembershipGroupScopeFactory => { + const getScopeField: TMembershipGroupScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const getScopeDatabaseFields: TMembershipGroupScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { scopeOrgId: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const isCustomRole: TMembershipGroupScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateMembershipGroupGuard: TMembershipGroupScopeFactory["onCreateMembershipGroupGuard"] = async () => { + throw new BadRequestError({ + message: "Organization membership cannot be created for groups" + }); + }; + + const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); + const permissionRoles = await permissionService.getOrgPermissionByRoles( + dto.data.roles.map((el) => el.role), + dto.permission.orgId + ); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== OrgMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create group org membership", + shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onDeleteMembershipGroupGuard: TMembershipGroupScopeFactory["onDeleteMembershipGroupGuard"] = async () => { + throw new BadRequestError({ + message: "Organization membership cannot be deleted for organization scoped group" + }); + }; + + const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); + }; + + const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); + }; + + return { + onCreateMembershipGroupGuard, + onUpdateMembershipGroupGuard, + onDeleteMembershipGroupGuard, + onListMembershipGroupGuard, + onGetMembershipGroupByGroupIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-group/project/project-membership-group-factory.ts b/backend/src/services/membership-group/project/project-membership-group-factory.ts new file mode 100644 index 0000000000..5a63eed1e2 --- /dev/null +++ b/backend/src/services/membership-group/project/project-membership-group-factory.ts @@ -0,0 +1,188 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionGroupActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TMembershipGroupDALFactory } from "../membership-group-dal"; +import { TMembershipGroupScopeFactory } from "../membership-group-types"; + +type TProjectMembershipGroupScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + membershipGroupDAL: Pick; +}; + +export const newProjectMembershipGroupFactory = ({ + permissionService, + orgDAL, + membershipGroupDAL +}: TProjectMembershipGroupScopeFactoryDep): TMembershipGroupScopeFactory => { + const getScopeField: TMembershipGroupScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const getScopeDatabaseFields: TMembershipGroupScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { scopeOrgId: dto.orgId, scopeProjectId: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const isCustomRole: TMembershipGroupScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role); + + const onCreateMembershipGroupGuard: TMembershipGroupScopeFactory["onCreateMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); + const orgMembership = await membershipGroupDAL.findOne({ + actorGroupId: dto.data.groupId, + scopeOrgId: dto.permission.orgId, + scope: AccessScope.Organization + }); + if (!orgMembership) + throw new BadRequestError({ message: `Group ${dto.data.groupId} is missing organization membership` }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.map((el) => el.role), + scope.value + ); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== ProjectMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create group project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.map((el) => el.role), + scope.value + ); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== ProjectMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update group project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onDeleteMembershipGroupGuard: TMembershipGroupScopeFactory["onDeleteMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); + }; + + const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + }; + + const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + }; + + return { + onCreateMembershipGroupGuard, + onUpdateMembershipGroupGuard, + onDeleteMembershipGroupGuard, + onListMembershipGroupGuard, + onGetMembershipGroupByGroupIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts new file mode 100644 index 0000000000..64e508fb8a --- /dev/null +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -0,0 +1,357 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessScope, AccessScopeData, MembershipsSchema, TableName } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; +import { TSearchResourceOperator } from "@app/lib/search-resource/search"; + +import { buildAuthMethods } from "../identity/identity-fns"; + +export type TMembershipIdentityDALFactory = ReturnType; + +type TFindIdentityArg = { + scopeData: AccessScopeData; + tx?: Knex; + filter: Partial<{ + limit: number; + offset: number; + identityId: string; + name: Omit; + role: Omit; + }>; +}; + +type TGetIdentityByIdArg = { + scopeData: AccessScopeData; + tx?: Knex; + identityId: string; +}; + +export const membershipIdentityDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + + const getIdentityById = async ({ scopeData, tx, identityId }: TGetIdentityByIdArg) => { + try { + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Membership}.actorIdentityId`, identityId) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .leftJoin( + TableName.IdentityUniversalAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityUniversalAuth}.identityId` + ) + .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) + .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`) + .leftJoin( + TableName.IdentityKubernetesAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`) + .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) + .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`) + .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) + .leftJoin( + TableName.IdentityTlsCertAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityTlsCertAuth}.identityId` + ) + .leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`) + .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), + + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), + db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), + db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), + db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), + db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), + db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), + db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), + db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), + db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), + db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { + identityId: actorIdentityId, + identityHasDeleteProtection, + identityName, + uaId, + awsId, + gcpId, + kubernetesId, + oidcId, + azureId, + alicloudId, + tokenId, + jwtId, + ociId, + ldapId, + tlsCertId + } = el; + return { + ...MembershipsSchema.parse(el), + identity: { + name: identityName, + id: actorIdentityId, + hasDeleteProtection: identityHasDeleteProtection, + authMethods: buildAuthMethods({ + uaId, + awsId, + gcpId, + kubernetesId, + oidcId, + azureId, + tokenId, + alicloudId, + jwtId, + ldapId, + ociId, + tlsCertId + }) + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipGetByIdentityId" }); + } + }; + + const findIdentities = async ({ scopeData, tx, filter }: TFindIdentityArg) => { + try { + const paginatedIdentitys = (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where((qb) => { + if (filter.identityId) { + void qb.where(`${TableName.Identity}.id`, filter.identityId); + } + + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }); + + if (filter.limit) void paginatedIdentitys.limit(filter.limit); + if (filter.offset) void paginatedIdentitys.offset(filter.offset); + + if (filter.name || filter.role) { + buildKnexFilterForSearchResource( + paginatedIdentitys, + { + name: filter.name!, + role: filter.role! + }, + (attr) => { + switch (attr) { + case "role": + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; + case "name": + return `${TableName.Identity}.name`; + default: + throw new BadRequestError({ message: `Invalid ${String(attr)} provided` }); + } + } + ); + } + + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .whereIn(`${TableName.Membership}.id`, paginatedIdentitys) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), + + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt") + ) + .select( + db.raw( + `count(${TableName.Membership}."actorIdentityId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total` + ) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { identityId: actorIdentityId, identityHasDeleteProtection, identityName } = el; + return { + ...MembershipsSchema.parse(el), + identity: { + name: identityName, + id: actorIdentityId, + hasDeleteProtection: identityHasDeleteProtection + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + return { data, totalCount: Number((data?.[0] as unknown as { total: number })?.total ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipfindIdentity" }); + } + }; + + return { ...orm, findIdentities, getIdentityById }; +}; diff --git a/backend/src/services/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts new file mode 100644 index 0000000000..4dd3da0647 --- /dev/null +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -0,0 +1,339 @@ +import { AccessScope, ProjectMembershipRole, TemporaryPermissionMode, TMembershipRolesInsert } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; +import { SearchResourceOperators } from "@app/lib/search-resource/search"; + +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TRoleDALFactory } from "../role/role-dal"; +import { TMembershipIdentityDALFactory } from "./membership-identity-dal"; +import { + TCreateMembershipIdentityDTO, + TDeleteMembershipIdentityDTO, + TGetMembershipIdentityByIdentityIdDTO, + TListMembershipIdentityDTO, + TUpdateMembershipIdentityDTO +} from "./membership-identity-types"; +import { newNamespaceMembershipIdentityFactory } from "./namespace/namespace-membership-identity-factory"; +import { newOrgMembershipIdentityFactory } from "./org/org-membership-identity-factory"; +import { newProjectMembershipIdentityFactory } from "./project/project-membership-identity-factory"; + +type TMembershipIdentityServiceFactoryDep = { + membershipIdentityDAL: TMembershipIdentityDALFactory; + membershipRoleDAL: Pick; + roleDAL: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getOrgPermission" | "getProjectPermission" | "getProjectPermissionByRoles" | "getOrgPermissionByRoles" + >; + orgDAL: Pick; + additionalPrivilegeDAL: Pick; +}; + +export type TMembershipIdentityServiceFactory = ReturnType; + +export const membershipIdentityServiceFactory = ({ + membershipIdentityDAL, + roleDAL, + membershipRoleDAL, + permissionService, + orgDAL, + additionalPrivilegeDAL +}: TMembershipIdentityServiceFactoryDep) => { + const scopeFactory = { + [AccessScope.Organization]: newOrgMembershipIdentityFactory({ + orgDAL, + permissionService + }), + [AccessScope.Project]: newProjectMembershipIdentityFactory({ + membershipIdentityDAL, + orgDAL, + permissionService + }), + [AccessScope.Namespace]: newNamespaceMembershipIdentityFactory({}) + }; + + const createMembership = async (dto: TCreateMembershipIdentityDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Identity must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + await factory.onCreateMembershipIdentityGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membership = await membershipIdentityDAL.transaction(async (tx) => { + const doc = await membershipIdentityDAL.create( + { + scope: scopeData.scope, + ...scopeDatabaseFields, + actorIdentityId: dto.data.identityId + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.insertMany(roleDocs, tx); + return doc; + }); + + return { membership }; + }; + + const updateMembership = async (dto: TUpdateMembershipIdentityDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onUpdateMembershipIdentityGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Identity must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipIdentityDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorIdentityId: dto.selector.identityId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Identity doesn't have membership" + }); + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipIdentityDAL.transaction(async (tx) => { + const doc = + typeof data.isActive === "undefined" + ? existingMembership + : await membershipIdentityDAL.updateById( + existingMembership.id, + { + isActive: data.isActive + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.delete( + { + membershipId: doc.id + }, + tx + ); + const insertedRoleDocs = await membershipRoleDAL.insertMany(roleDocs, tx); + return { ...doc, roles: insertedRoleDocs }; + }); + + return { membership: membershipDoc }; + }; + + const deleteMembership = async (dto: TDeleteMembershipIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onDeleteMembershipIdentityGuard(dto); + + const scopeField = factory.getScopeField(scopeData); + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipIdentityDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorIdentityId: dto.selector.identityId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Identity doesn't have membership" + }); + + if (existingMembership.actorIdentityId === dto.permission.id) + throw new BadRequestError({ + message: "You can't delete your own membership" + }); + + const membershipDoc = await membershipIdentityDAL.transaction(async (tx) => { + await additionalPrivilegeDAL.delete( + { + actorIdentityId: dto.selector.identityId, + [scopeField.key]: scopeField.value + }, + tx + ); + await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx); + const doc = await membershipIdentityDAL.deleteById(existingMembership.id, tx); + return doc; + }); + return { membership: membershipDoc }; + }; + + const listMemberships = async (dto: TListMembershipIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipIdentityGuard(dto); + const memberships = await membershipIdentityDAL.findIdentities({ + scopeData, + filter: { + limit: dto.data.limit, + offset: dto.data.offset, + name: dto.data.identityName + ? { + [SearchResourceOperators.$contains]: dto.data.identityName + } + : undefined, + role: dto.data.roles.length + ? { + [SearchResourceOperators.$in]: dto.data.roles + } + : undefined + } + }); + return memberships; + }; + + const getMembershipByIdentityId = async (dto: TGetMembershipIdentityByIdentityIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetMembershipIdentityByIdentityIdGuard(dto); + const membership = await membershipIdentityDAL.getIdentityById({ + scopeData, + identityId: selector.identityId + }); + if (!membership) throw new NotFoundError({ message: `Identity membership not found` }); + + return membership; + }; + + return { + createMembership, + updateMembership, + deleteMembership, + listMemberships, + getMembershipByIdentityId + }; +}; diff --git a/backend/src/services/membership-identity/membership-identity-types.ts b/backend/src/services/membership-identity/membership-identity-types.ts new file mode 100644 index 0000000000..adce10237b --- /dev/null +++ b/backend/src/services/membership-identity/membership-identity-types.ts @@ -0,0 +1,82 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TMembershipIdentityScopeFactory { + onCreateMembershipIdentityGuard: (arg: TCreateMembershipIdentityDTO) => Promise; + + onUpdateMembershipIdentityGuard: (arg: TUpdateMembershipIdentityDTO) => Promise; + onDeleteMembershipIdentityGuard: (arg: TDeleteMembershipIdentityDTO) => Promise; + onListMembershipIdentityGuard: (arg: TListMembershipIdentityDTO) => Promise; + onGetMembershipIdentityByIdentityIdGuard: (arg: TGetMembershipIdentityByIdentityIdDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + getScopeDatabaseFields: (scope: AccessScopeData) => { + scopeOrgId: string; + scopeNamespaceId?: string | null; + scopeProjectId?: string | null; + }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + identityId: string; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TUpdateMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; + data: { + isActive?: boolean; + metadata?: { key: string; value: string }[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TListMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; + data: { + limit?: number; + offset?: number; + identityName?: string; + roles: string[]; + }; +}; + +export type TDeleteMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; +}; + +export type TGetMembershipIdentityByIdentityIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; +}; diff --git a/backend/src/services/membership-identity/namespace/namespace-membership-identity-factory.ts b/backend/src/services/membership-identity/namespace/namespace-membership-identity-factory.ts new file mode 100644 index 0000000000..be3ca6ea9b --- /dev/null +++ b/backend/src/services/membership-identity/namespace/namespace-membership-identity-factory.ts @@ -0,0 +1,64 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; + +type TNamespaceMembershipIdentityScopeFactoryDep = Record; + +export const newNamespaceMembershipIdentityFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceMembershipIdentityScopeFactoryDep +): TMembershipIdentityScopeFactory => { + const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const getScopeDatabaseFields: TMembershipIdentityScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { scopeOrgId: dto.orgId, scopeNamespaceId: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = () => { + throw new InternalServerError({ message: "Namespace membership identity isCustomRole not implemented" }); + }; + + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity create not implemented" }); + }; + + const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity update not implemented" }); + }; + + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity delete not implemented" }); + }; + + const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership identity list not implemented" }); + }; + + const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity get by identity id not implemented" }); + }; + + return { + onCreateMembershipIdentityGuard, + onUpdateMembershipIdentityGuard, + onDeleteMembershipIdentityGuard, + onListMembershipIdentityGuard, + onGetMembershipIdentityByIdentityIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts new file mode 100644 index 0000000000..06789e274a --- /dev/null +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -0,0 +1,130 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; + +import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; + +type TOrgMembershipIdentityScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; +}; + +export const newOrgMembershipIdentityFactory = ({ + permissionService, + orgDAL +}: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { + const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const getScopeDatabaseFields: TMembershipIdentityScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { scopeOrgId: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = + async () => { + throw new BadRequestError({ + message: "Organization membership cannot be created for organization scoped identity" + }); + }; + + const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + const permissionRoles = await permissionService.getOrgPermissionByRoles( + dto.data.roles.map((el) => el.role), + dto.permission.orgId + ); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== OrgMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update identity org membership", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = + async () => { + throw new BadRequestError({ + message: "Organization membership cannot be deleted for organization scoped identity" + }); + }; + + const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + }; + + const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = + async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + }; + + return { + onCreateMembershipIdentityGuard, + onUpdateMembershipIdentityGuard, + onDeleteMembershipIdentityGuard, + onListMembershipIdentityGuard, + onGetMembershipIdentityByIdentityIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-identity/project/project-membership-identity-factory.ts b/backend/src/services/membership-identity/project/project-membership-identity-factory.ts new file mode 100644 index 0000000000..f2896047f3 --- /dev/null +++ b/backend/src/services/membership-identity/project/project-membership-identity-factory.ts @@ -0,0 +1,208 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionIdentityActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TMembershipIdentityDALFactory } from "../membership-identity-dal"; +import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; + +type TProjectMembershipIdentityScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + membershipIdentityDAL: Pick; +}; + +export const newProjectMembershipIdentityFactory = ({ + permissionService, + orgDAL, + membershipIdentityDAL +}: TProjectMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { + const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const getScopeDatabaseFields: TMembershipIdentityScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { scopeOrgId: dto.orgId, scopeProjectId: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role); + + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + ProjectPermissionSub.Identity + ); + const orgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: dto.data.identityId, + scopeOrgId: dto.permission.orgId, + scope: AccessScope.Organization + }); + if (!orgMembership) + throw new BadRequestError({ message: `Identity ${dto.data.identityId} is missing organization membership` }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.map((el) => el.role), + scope.value + ); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== ProjectMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create identity project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + ProjectPermissionSub.Identity + ); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.filter((el) => el.role !== ProjectMembershipRole.NoAccess).map((el) => el.role), + scope.value + ); + for (const permissionRole of permissionRoles) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update identity project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + }; + + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Delete, + ProjectPermissionSub.Identity + ); + }; + + const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity + ); + }; + + const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity + ); + }; + + return { + onCreateMembershipIdentityGuard, + onUpdateMembershipIdentityGuard, + onDeleteMembershipIdentityGuard, + onListMembershipIdentityGuard, + onGetMembershipIdentityByIdentityIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts new file mode 100644 index 0000000000..7882b9639c --- /dev/null +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -0,0 +1,295 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessScope, AccessScopeData, MembershipsSchema, TableName } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; +import { TSearchResourceOperator } from "@app/lib/search-resource/search"; + +export type TMembershipUserDALFactory = ReturnType; + +type TFindUserArg = { + scopeData: AccessScopeData; + tx?: Knex; + filter: Partial<{ + limit: number; + offset: number; + userId?: string; + username: Omit; + role: Omit; + }>; +}; + +type TGetUserByIdArg = { + scopeData: AccessScopeData; + tx?: Knex; + userId: string; +}; + +export const membershipUserDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + + const getUserById = async ({ scopeData, tx, userId }: TGetUserByIdArg) => { + try { + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Membership}.actorUserId`, userId) + .where(`${TableName.Users}.isGhost`, false) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("id").withSchema(TableName.Role).as("customRoleId"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), + db.ref("username").withSchema(TableName.Users).as("userUsername"), + db.ref("email").withSchema(TableName.Users).as("userEmail"), + db.ref("firstName").withSchema(TableName.Users).as("userFirstName"), + db.ref("lastName").withSchema(TableName.Users).as("userLastName"), + db.ref("id").withSchema(TableName.Users).as("userId") + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + ...MembershipsSchema.parse(el), + user: { + username: el.userUsername, + email: el.userEmail, + firstName: el.userFirstName, + lastName: el.userLastName, + id: el.userId + } + }), + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + customRoleSlug, + customRoleName, + customRoleId, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug, + customRoleName, + customRoleId, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipGetByUserId" }); + } + }; + + const findUsers = async ({ scopeData, tx, filter }: TFindUserArg) => { + try { + const paginatedUsers = (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Users}.isGhost`, false) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }); + + if (filter.limit) void paginatedUsers.limit(filter.limit); + if (filter.offset) void paginatedUsers.offset(filter.offset); + + if (filter.username || filter.role) { + buildKnexFilterForSearchResource( + paginatedUsers, + { + username: filter.username!, + role: filter.role! + }, + (attr) => { + switch (attr) { + case "role": + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; + case "username": + return `${TableName.Users}.username`; + default: + throw new BadRequestError({ message: `Invalid ${String(attr)} provided` }); + } + } + ); + } + + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .whereIn(`${TableName.Membership}.id`, paginatedUsers) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("id").withSchema(TableName.Role).as("customRoleId"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("username").withSchema(TableName.Users).as("userUsername"), + db.ref("email").withSchema(TableName.Users).as("userEmail"), + db.ref("firstName").withSchema(TableName.Users).as("userFirstName"), + db.ref("lastName").withSchema(TableName.Users).as("userLastName"), + db.ref("id").withSchema(TableName.Users).as("userId") + ) + .select( + db.raw( + `count(${TableName.Membership}."actorUserId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total` + ) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + ...MembershipsSchema.parse(el), + user: { + username: el.userUsername, + email: el.userEmail, + firstName: el.userFirstName, + lastName: el.userLastName, + id: el.userId + } + }), + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + customRoleSlug, + customRoleName, + customRoleId, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug, + customRoleName, + customRoleId, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + return { data, totalCount: Number((data?.[0] as unknown as { total: number })?.total ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipfindUser" }); + } + }; + + return { ...orm, findUsers, getUserById }; +}; diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts new file mode 100644 index 0000000000..82dca01596 --- /dev/null +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -0,0 +1,481 @@ +import { + AccessScope, + OrgMembershipStatus, + ProjectMembershipRole, + TemporaryPermissionMode, + TMembershipRolesInsert +} from "@app/db/schemas"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; +import { SearchResourceOperators } from "@app/lib/search-resource/search"; + +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { AuthMethod } from "../auth/auth-type"; +import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { deleteOrgMembershipsFn } from "../org/org-fns"; +import { TProjectDALFactory } from "../project/project-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; +import { TRoleDALFactory } from "../role/role-dal"; +import { TSmtpService } from "../smtp/smtp-service"; +import { TUserDALFactory } from "../user/user-dal"; +import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; +import { TMembershipUserDALFactory } from "./membership-user-dal"; +import { + TCreateMembershipUserDTO, + TDeleteMembershipUserDTO, + TGetMembershipUserByUserIdDTO, + TListMembershipUserDTO, + TUpdateMembershipUserDTO +} from "./membership-user-types"; +import { newNamespaceMembershipUserFactory } from "./namespace/namespace-membership-user-factory"; +import { newOrgMembershipUserFactory } from "./org/org-membership-user-factory"; +import { newProjectMembershipUserFactory } from "./project/project-membership-user-factory"; + +type TMembershipUserServiceFactoryDep = { + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: Pick; + orgDAL: Pick; + roleDAL: Pick; + userDAL: TUserDALFactory; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "getProjectPermissionByRoles" | "getOrgPermission" + >; + licenseService: TLicenseServiceFactory; + projectKeyDAL: TProjectKeyDALFactory; + userAliasDAL: TUserAliasDALFactory; + smtpService: TSmtpService; + tokenService: TAuthTokenServiceFactory; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; + projectDAL: TProjectDALFactory; + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; +}; + +export type TMembershipUserServiceFactory = ReturnType; + +export const membershipUserServiceFactory = ({ + membershipUserDAL, + roleDAL, + membershipRoleDAL, + userDAL, + permissionService, + orgDAL, + projectKeyDAL, + userAliasDAL, + licenseService, + smtpService, + tokenService, + userGroupMembershipDAL, + projectDAL, + additionalPrivilegeDAL +}: TMembershipUserServiceFactoryDep) => { + const scopeFactory = { + [AccessScope.Organization]: newOrgMembershipUserFactory({ + permissionService, + licenseService, + smtpService, + orgDAL, + tokenService, + userDAL, + userGroupMembershipDAL + }), + [AccessScope.Namespace]: newNamespaceMembershipUserFactory({}), + [AccessScope.Project]: newProjectMembershipUserFactory({ + orgDAL, + permissionService, + membershipUserDAL, + projectDAL, + smtpService + }) + }; + + const $getUsers = async (usernames: string[]) => { + const existingUsers = await userDAL.find({ $in: { username: usernames } }); + if (existingUsers.length !== usernames.length) { + const newUserEmails = usernames.filter( + (inviteeEmail) => !existingUsers.find((el) => el.username === inviteeEmail) + ); + await userDAL.transaction(async (tx) => { + for await (const inviteeEmail of newUserEmails) { + const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx); + let inviteeUser = + usersByUsername?.length > 1 + ? usersByUsername.find((el) => el.username === inviteeEmail) + : usersByUsername?.[0]; + + // if the user doesn't exist we create the user with the email + if (!inviteeUser) { + // TODO(carlos): will be removed once the function receives usernames instead of emails + const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx); + if (usersByEmail?.length === 1) { + [inviteeUser] = usersByEmail; + } else { + inviteeUser = await userDAL.create( + { + isAccepted: false, + email: inviteeEmail, + username: inviteeEmail, + authMethods: [AuthMethod.EMAIL], + isGhost: false + }, + tx + ); + } + } + + existingUsers.push(inviteeUser); + const inviteeUserId = inviteeUser?.id; + const existingEncryptionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx); + + // when user is missing the encrytion keys + // this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp + // So what we do is we generate a random secure password and then encrypt it with a random pub-private key + // Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password + if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncryptionKey)) { + await userDAL.createUserEncryption( + { + userId: inviteeUserId, + encryptionVersion: 2 + }, + tx + ); + } + } + }); + } + return existingUsers; + }; + + const createMembership = async (dto: TCreateMembershipUserDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "User must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const users = await $getUsers(dto.data.usernames); + const existingMemberships = await membershipUserDAL.find({ + scope: scopeData.scope, + ...scopeDatabaseFields, + $in: { + actorUserId: users.map((el) => el.id) + } + }); + + if (existingMemberships.length === users.length) return { memberships: [] }; + + const newMembershipUsers = users.filter((user) => !existingMemberships?.find((el) => el.actorUserId === user.id)); + await factory.onCreateMembershipUserGuard(dto, newMembershipUsers); + const newMemberships = newMembershipUsers.map((user) => ({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorUserId: user.id, + status: scopeData.scope === AccessScope.Organization ? OrgMembershipStatus.Invited : undefined, + inviteEmail: scopeData.scope === AccessScope.Organization ? user.email : undefined + })); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + if (hasCustomRole) { + const plan = await licenseService.getPlan(scopeData.orgId); + if (!plan?.rbac) + throw new BadRequestError({ + message: + "Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role." + }); + } + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipUserDAL.transaction(async (tx) => { + const docs = await membershipUserDAL.insertMany(newMemberships, tx); + + const roleDocs: TMembershipRolesInsert[] = []; + docs.forEach((membership) => { + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: membership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: membership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + }); + await membershipRoleDAL.insertMany(roleDocs, tx); + return docs; + }); + + const { signUpTokens } = await factory.onCreateMembershipComplete(dto, newMembershipUsers); + return { memberships: membershipDoc, signUpTokens }; + }; + + const updateMembership = async (dto: TUpdateMembershipUserDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onUpdateMembershipUserGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + if (hasCustomRole) { + const plan = await licenseService.getPlan(scopeData.orgId); + if (!plan?.rbac) + throw new BadRequestError({ + message: + "Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role." + }); + } + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "User must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipUserDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorUserId: dto.selector.userId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "User doesn't have membership" + }); + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipUserDAL.transaction(async (tx) => { + const doc = + typeof data?.isActive === "undefined" + ? existingMembership + : await membershipUserDAL.updateById( + existingMembership.id, + { + isActive: data.isActive + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.delete( + { + membershipId: doc.id + }, + tx + ); + const insertedRoles = await membershipRoleDAL.insertMany(roleDocs, tx); + return { ...doc, roles: insertedRoles }; + }); + + return { membership: membershipDoc }; + }; + + const deleteMembership = async (dto: TDeleteMembershipUserDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onDeleteMembershipUserGuard(dto); + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipUserDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorUserId: dto.selector.userId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "User doesn't have membership" + }); + + if (existingMembership.actorUserId === dto.permission.id) + throw new BadRequestError({ + message: "You can't delete your own membership" + }); + + const membershipDoc = await membershipUserDAL.transaction(async (tx) => { + if (dto.scopeData.scope === AccessScope.Organization) { + const [doc] = await deleteOrgMembershipsFn({ + orgMembershipIds: [], + orgId: dto.permission.orgId, + orgDAL, + projectKeyDAL, + userAliasDAL, + licenseService, + userId: dto.permission.id, + membershipUserDAL, + userGroupMembershipDAL, + membershipRoleDAL, + additionalPrivilegeDAL + }); + return doc; + } + + if (dto.scopeData.scope === AccessScope.Project) { + await additionalPrivilegeDAL.delete( + { + actorUserId: dto.selector.userId, + projectId: dto.scopeData.projectId + }, + tx + ); + } + + await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx); + const doc = await membershipUserDAL.deleteById(existingMembership.id, tx); + return doc; + }); + return { membership: membershipDoc }; + }; + + const listMemberships = async (dto: TListMembershipUserDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipUserGuard(dto); + const memberships = await membershipUserDAL.findUsers({ + scopeData, + filter: { + limit: dto.data.limit, + offset: dto.data.offset, + username: dto.data.username, + role: dto.data?.roles?.length + ? { + [SearchResourceOperators.$in]: dto.data.roles + } + : undefined + } + }); + return memberships; + }; + + const getMembershipByUserId = async (dto: TGetMembershipUserByUserIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetMembershipUserByUserIdGuard(dto); + const membership = await membershipUserDAL.getUserById({ + scopeData, + userId: selector.userId + }); + if (!membership) throw new NotFoundError({ message: `User membership not found` }); + + return membership; + }; + + return { + createMembership, + updateMembership, + deleteMembership, + listMemberships, + getMembershipByUserId + }; +}; diff --git a/backend/src/services/membership-user/membership-user-types.ts b/backend/src/services/membership-user/membership-user-types.ts new file mode 100644 index 0000000000..b8761671c6 --- /dev/null +++ b/backend/src/services/membership-user/membership-user-types.ts @@ -0,0 +1,95 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TMembershipUserScopeFactory { + onCreateMembershipUserGuard: ( + arg: TCreateMembershipUserDTO, + newMembers: { id: string; email?: string | null }[] + ) => Promise; + onCreateMembershipComplete: ( + arg: TCreateMembershipUserDTO, + newMembers: { id: string; email?: string | null }[] + ) => Promise<{ signUpTokens: { email: string; link: string }[] }>; + + onUpdateMembershipUserGuard: (arg: TUpdateMembershipUserDTO) => Promise; + onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO | TBulkDeleteMembershipByUsernameDTO) => Promise; + + onListMembershipUserGuard: (arg: TListMembershipUserDTO) => Promise; + onGetMembershipUserByUserIdGuard: (arg: TGetMembershipUserByUserIdDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + getScopeDatabaseFields: (scope: AccessScopeData) => { + scopeOrgId: string; + scopeNamespaceId?: string | null; + scopeProjectId?: string | null; + }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + usernames: string[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TUpdateMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + userId: string; + }; + data: { + isActive?: boolean; + metadata?: { key: string; value: string }[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TListMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + limit?: number; + offset?: number; + username?: string; + roles?: string[]; + }; +}; + +export type TDeleteMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + userId: string; + }; +}; + +export type TBulkDeleteMembershipByUsernameDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + usernames: string[]; + }; +}; + +export type TGetMembershipUserByUserIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + userId: string; + }; +}; diff --git a/backend/src/services/membership-user/namespace/namespace-membership-user-factory.ts b/backend/src/services/membership-user/namespace/namespace-membership-user-factory.ts new file mode 100644 index 0000000000..fb2cfc463e --- /dev/null +++ b/backend/src/services/membership-user/namespace/namespace-membership-user-factory.ts @@ -0,0 +1,66 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TMembershipUserScopeFactory } from "../membership-user-types"; + +type TNamespaceMembershipUserScopeFactoryDep = Record; + +export const newNamespaceMembershipUserFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceMembershipUserScopeFactoryDep +): TMembershipUserScopeFactory => { + const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { scopeOrgId: dto.orgId, scopeNamespaceId: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = () => { + throw new InternalServerError({ message: "Namespace membership user isCustomRole not implemented" }); + }; + + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user create not implemented" }); + }; + + const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async () => { + throw new InternalServerError({ message: "Namespace membership user create complete not implemented" }); + }; + + const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user update not implemented" }); + }; + + const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user delete not implemented" }); + }; + + const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user list not implemented" }); + }; + + const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership user get by user id not implemented" }); + }; + + return { + onCreateMembershipUserGuard, + onCreateMembershipComplete, + onUpdateMembershipUserGuard, + onDeleteMembershipUserGuard, + onListMembershipUserGuard, + onGetMembershipUserByUserIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts new file mode 100644 index 0000000000..523e85baeb --- /dev/null +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -0,0 +1,193 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope } from "@app/db/schemas"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +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-types"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; +import { ActorType } 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 { TOrgDALFactory } from "@app/services/org/org-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; + +import { TMembershipUserScopeFactory } from "../membership-user-types"; + +type TOrgMembershipUserScopeFactoryDep = { + permissionService: Pick; + tokenService: Pick; + userDAL: Pick; + smtpService: Pick; + orgDAL: Pick; + userGroupMembershipDAL: Pick; + licenseService: Pick; +}; + +export const newOrgMembershipUserFactory = ({ + permissionService, + tokenService, + userDAL, + orgDAL, + smtpService, + licenseService +}: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { + const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { scopeOrgId: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); + + const plan = await licenseService.getPlan(dto.permission.orgId); + if (plan?.slug !== "enterprise" && 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({ + name: "InviteUser", + message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." + }); + } + + const org = await orgDAL.findById(dto.permission.orgId); + if (org?.authEnforced) { + throw new ForbiddenRequestError({ + name: "InviteUser", + message: "Failed to invite user due to org-level auth enforced for organization" + }); + } + }; + + const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async ( + dto, + newUsers + ) => { + const appCfg = getConfig(); + + const actorDetails = + dto.permission.type === ActorType.USER + ? await userDAL.findById(dto.permission.id) + : { + firstName: "Platform Identity", + email: "identity" + }; + + const signUpTokens: { email: string; link: string }[] = []; + const orgDetails = await orgDAL.findById(dto.permission.orgId); + + await Promise.allSettled( + newUsers.map(async (el) => { + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_ORG_INVITATION, + userId: el.id, + orgId: dto.permission.orgId + }); + + if (el.email) { + if (!appCfg.isSmtpConfigured) { + signUpTokens.push({ + email: el.email, + link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${dto.permission.orgId}` + }); + } + + await smtpService.sendMail({ + template: SmtpTemplates.OrgInvite, + subjectLine: "Infisical organization invitation", + recipients: [el.email], + substitutions: { + inviterFirstName: actorDetails?.firstName, + inviterUsername: actorDetails?.email, + organizationName: orgDetails?.name, + email: el.email, + organizationId: orgDetails?.id.toString(), + token, + callback_url: `${appCfg.SITE_URL}/signupinvite` + } + }); + } + }) + ); + + return { signUpTokens }; + }; + + const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); + }; + + const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); + }; + + const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + }; + + const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + }; + + return { + onCreateMembershipUserGuard, + onCreateMembershipComplete, + onUpdateMembershipUserGuard, + onDeleteMembershipUserGuard, + onListMembershipUserGuard, + onGetMembershipUserByUserIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-user/project/project-membership-user-factory.ts b/backend/src/services/membership-user/project/project-membership-user-factory.ts new file mode 100644 index 0000000000..0a9bb955af --- /dev/null +++ b/backend/src/services/membership-user/project/project-membership-user-factory.ts @@ -0,0 +1,239 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType, OrgMembershipStatus, ProjectMembershipRole } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionMemberActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; + +import { TMembershipUserDALFactory } from "../membership-user-dal"; +import { TMembershipUserScopeFactory } from "../membership-user-types"; + +type TProjectMembershipUserScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + projectDAL: Pick; + membershipUserDAL: Pick; + smtpService: Pick; +}; + +export const newProjectMembershipUserFactory = ({ + permissionService, + orgDAL, + projectDAL, + membershipUserDAL, + smtpService +}: TProjectMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { + const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { scopeOrgId: dto.orgId, scopeProjectId: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role); + + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async ( + dto, + newUsers + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); + + // TODO(namespace): this becomes tricky in namespace due to group flow + const orgMemberships = await membershipUserDAL.find({ + scope: AccessScope.Organization, + scopeOrgId: dto.permission.orgId, + $in: { + actorUserId: newUsers.map((el) => el.id) + } + }); + if (orgMemberships.length !== newUsers.length) { + const missingUsers = newUsers + .filter((el) => !orgMemberships.find((memb) => memb.actorUserId === el.id)) + .map((el) => el.email); + throw new BadRequestError({ message: `Users ${missingUsers.join(",")} not part of organization` }); + } + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.filter((el) => el.role !== ProjectMembershipRole.NoAccess).map((el) => el.role), + scope.value + ); + + for (const permissionRole of permissionRoles) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create user project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + }; + + const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async ( + dto, + newMembers + ) => { + const orgMembershipAccepted = await membershipUserDAL.find({ + scope: AccessScope.Organization, + scopeOrgId: dto.permission.orgId, + status: OrgMembershipStatus.Accepted, + $in: { + actorUserId: newMembers.map((el) => el.id) + } + }); + + if (!orgMembershipAccepted.length) return { signUpTokens: [] }; + + const appCfg = getConfig(); + const scope = getScopeField(dto.scopeData); + const project = await projectDAL.findById(scope.value); + + const orgMembershipAcceptedUserIds = orgMembershipAccepted.map((el) => el.actorUserId as string); + const emails = newMembers + .filter((el) => Boolean(el?.email) && orgMembershipAcceptedUserIds.includes(el.id)) + .map((el) => el?.email as string); + if (emails.length) { + await smtpService.sendMail({ + template: SmtpTemplates.WorkspaceInvite, + subjectLine: "Infisical project invitation", + recipients: emails, + substitutions: { + workspaceName: project.name, + callback_url: `${appCfg.SITE_URL}/login` + } + }); + } + return { signUpTokens: [] }; + }; + + const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.filter((el) => el.role !== ProjectMembershipRole.NoAccess).map((el) => el.role), + scope.value + ); + + for (const permissionRole of permissionRoles) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update user project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + }; + + const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); + }; + + const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + }; + + const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + }; + + return { + onCreateMembershipUserGuard, + onCreateMembershipComplete, + onUpdateMembershipUserGuard, + onDeleteMembershipUserGuard, + onListMembershipUserGuard, + onGetMembershipUserByUserIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership/membership-dal.ts b/backend/src/services/membership/membership-dal.ts new file mode 100644 index 0000000000..03ee6af924 --- /dev/null +++ b/backend/src/services/membership/membership-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TMembershipDALFactory = ReturnType; + +export const membershipDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + return orm; +}; diff --git a/backend/src/services/membership/membership-role-dal.ts b/backend/src/services/membership/membership-role-dal.ts new file mode 100644 index 0000000000..bb70112215 --- /dev/null +++ b/backend/src/services/membership/membership-role-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TMembershipRoleDALFactory = ReturnType; + +export const membershipRoleDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.MembershipRole); + return orm; +}; diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index a3657c6800..84cf35a508 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -15,7 +15,9 @@ export enum NotificationType { DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin", PROJECT_ACCESS_REQUEST = "project-access-request", PROJECT_INVITATION = "project-invitation", - SECRET_SYNC_FAILED = "secret-sync-failed" + SECRET_SYNC_FAILED = "secret-sync-failed", + GATEWAY_HEALTH_ALERT = "gateway-health-alert", + RELAY_HEALTH_ALERT = "relay-health-alert" } export interface TCreateUserNotificationDTO { diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 995afadc14..4c080717d9 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,26 +1,25 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TNotificationServiceFactory } from "../notification/notification-service"; import { NotificationType } from "../notification/notification-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; type TOrgAdminServiceFactoryDep = { permissionService: Pick; projectDAL: Pick; - projectMembershipDAL: Pick< - TProjectMembershipDALFactory, - "findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers" - >; - projectUserMembershipRoleDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; smtpService: Pick; notificationService: Pick; }; @@ -31,9 +30,10 @@ export const orgAdminServiceFactory = ({ permissionService, projectDAL, projectMembershipDAL, - projectUserMembershipRoleDAL, smtpService, - notificationService + notificationService, + membershipUserDAL, + membershipRoleDAL }: TOrgAdminServiceFactoryDep) => { const listOrgProjects = async ({ actor, @@ -98,17 +98,18 @@ export const orgAdminServiceFactory = ({ } // check already there exist a membership if there return it - const projectMembership = await projectMembershipDAL.findOne({ - projectId, - userId: actorId + const projectMembership = await membershipUserDAL.findOne({ + scopeProjectId: projectId, + scope: AccessScope.Project, + actorUserId: actorId }); if (projectMembership) { // reset and make the user admin - await projectMembershipDAL.transaction(async (tx) => { - await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx); - await projectUserMembershipRoleDAL.create( + await membershipUserDAL.transaction(async (tx) => { + await membershipRoleDAL.delete({ membershipId: projectMembership.id }, tx); + await membershipRoleDAL.create( { - projectMembershipId: projectMembership.id, + membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, tx @@ -117,18 +118,17 @@ export const orgAdminServiceFactory = ({ return { isExistingMember: true, membership: projectMembership }; } - const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { - const newProjectMembership = await projectMembershipDAL.create( + const updatedMembership = await membershipUserDAL.transaction(async (tx) => { + const newProjectMembership = await membershipUserDAL.create( { - projectId, - userId: actorId + scopeProjectId: projectId, + actorUserId: actorId, + scope: AccessScope.Project, + scopeOrgId: actorOrgId }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); + await membershipRoleDAL.create({ membershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin }, tx); return newProjectMembership; }); diff --git a/backend/src/services/org-membership/org-membership-dal.ts b/backend/src/services/org-membership/org-membership-dal.ts index 2d7992d346..c52c2834a8 100644 --- a/backend/src/services/org-membership/org-membership-dal.ts +++ b/backend/src/services/org-membership/org-membership-dal.ts @@ -1,19 +1,21 @@ import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, sqlNestRelationships } from "@app/lib/knex"; +import { sqlNestRelationships } from "@app/lib/knex"; export type TOrgMembershipDALFactory = ReturnType; export const orgMembershipDALFactory = (db: TDbClient) => { - const orgMembershipOrm = ormify(db, TableName.OrgMembership); - const findOrgMembershipById = async (membershipId: string) => { try { const member = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.id`, membershipId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.id`, membershipId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.Role}.id`, `${TableName.MembershipRole}.customRoleId`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, @@ -21,19 +23,20 @@ export const orgMembershipDALFactory = (db: TDbClient) => { ) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("inviteEmail").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("isActive").withSchema(TableName.OrgMembership), - db.ref("lastLoginAuthMethod").withSchema(TableName.OrgMembership), - db.ref("lastLoginTime").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("inviteEmail").withSchema(TableName.Membership), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("status").withSchema(TableName.Membership), + db.ref("isActive").withSchema(TableName.Membership), + db.ref("lastLoginAuthMethod").withSchema(TableName.Membership), + db.ref("lastLoginTime").withSchema(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), @@ -55,6 +58,7 @@ export const orgMembershipDALFactory = (db: TDbClient) => { parentMapper: ({ email, isEmailVerified, + customRoleSlug, username, firstName, lastName, @@ -74,6 +78,7 @@ export const orgMembershipDALFactory = (db: TDbClient) => { orgId, id, role, + customRoleSlug, status, isActive, inviteEmail, @@ -117,18 +122,20 @@ export const orgMembershipDALFactory = (db: TDbClient) => { const twelveMonthsAgo = new Date(now.getTime() - 360 * 24 * 60 * 60 * 1000); const memberships = await db - .replicaNode()(TableName.OrgMembership) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) .where("status", "invited") .where((qb) => { // lastInvitedAt is null AND createdAt is between 1 week and 12 months ago void qb - .whereNull(`${TableName.OrgMembership}.lastInvitedAt`) - .whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]); + .whereNull(`${TableName.Membership}.lastInvitedAt`) + .whereBetween(`${TableName.Membership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]); // lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago void qb.orWhere((qbInner) => { void qbInner - .where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo) - .where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo); + .where(`${TableName.Membership}.lastInvitedAt`, "<", oneWeekAgo) + .where(`${TableName.Membership}.createdAt`, ">", oneMonthAgo); }); }); @@ -144,7 +151,11 @@ export const orgMembershipDALFactory = (db: TDbClient) => { const updateLastInvitedAtByIds = async (membershipIds: string[]) => { try { if (membershipIds.length === 0) return; - await db(TableName.OrgMembership).whereIn("id", membershipIds).update({ lastInvitedAt: new Date() }); + await db(TableName.Membership) + .whereIn("id", membershipIds) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .update({ lastInvitedAt: new Date() }); } catch (error) { throw new DatabaseError({ error, @@ -156,9 +167,12 @@ export const orgMembershipDALFactory = (db: TDbClient) => { const findOrgMembershipsWithUsersByOrgId = async (orgId: string) => { try { const members = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, @@ -166,17 +180,17 @@ export const orgMembershipDALFactory = (db: TDbClient) => { ) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("inviteEmail").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("isActive").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("inviteEmail").withSchema(TableName.Membership), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("customRoleId"), + db.ref("status").withSchema(TableName.Membership), + db.ref("isActive").withSchema(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), @@ -207,7 +221,6 @@ export const orgMembershipDALFactory = (db: TDbClient) => { }; return { - ...orgMembershipOrm, findOrgMembershipById, findRecentInvitedMemberships, updateLastInvitedAtByIds, diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 925784bfff..288926f90c 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -2,14 +2,15 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, OrganizationsSchema, OrgMembershipRole, TableName, + TMemberships, + TMembershipsInsert, + TMembershipsUpdate, TOrganizations, TOrganizationsInsert, - TOrgMemberships, - TOrgMembershipsInsert, - TOrgMembershipsUpdate, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; @@ -43,8 +44,6 @@ export const orgDALFactory = (db: TDbClient) => { sortBy?: keyof TOrganizations; }) => { try { - const query = db.replicaNode()(TableName.Organization); - // Build the subquery for limited organization IDs const orgSubquery = db.replicaNode().select("id").from(TableName.Organization); @@ -54,37 +53,48 @@ export const orgDALFactory = (db: TDbClient) => { }); } + const countQuery = orgSubquery.clone(); + if (sortBy) { void orgSubquery.orderBy(sortBy); } - void orgSubquery.limit(limit).offset(offset); - // Main query with joins, limited to the subquery results - const docs = await query - .whereIn(`${TableName.Organization}.id`, orgSubquery) - .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) - .leftJoin(TableName.OrgMembership, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) - .leftJoin(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`) - .where((qb) => { - void qb.where(`${TableName.Users}.isGhost`, false).orWhereNull(`${TableName.Users}.id`); - }) - .select(selectAllTableCols(TableName.Organization)) - .select(db.ref("name").withSchema(TableName.Project).as("projectName")) - .select(db.ref("id").withSchema(TableName.Project).as("projectId")) - .select(db.ref("slug").withSchema(TableName.Project).as("projectSlug")) - .select(db.ref("createdAt").withSchema(TableName.Project).as("projectCreatedAt")) - .select(db.ref("email").withSchema(TableName.Users).as("userEmail")) - .select(db.ref("username").withSchema(TableName.Users).as("username")) - .select(db.ref("firstName").withSchema(TableName.Users).as("firstName")) - .select(db.ref("lastName").withSchema(TableName.Users).as("lastName")) - .select(db.ref("id").withSchema(TableName.Users).as("userId")) - .select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId")) - .select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole")) - .select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId")) - .select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus")) - .select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName")); + const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => { + return db + .replicaNode()(TableName.Organization) + .whereIn(`${TableName.Organization}.id`, orgIdSubquery) + .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) + .leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .leftJoin(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .leftJoin(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .where((qb) => { + void qb.where(`${TableName.Users}.isGhost`, false).orWhereNull(`${TableName.Users}.id`); + }); + }; + + const [docs, totalResult] = await Promise.all([ + buildBaseQuery(orgSubquery) + .select(selectAllTableCols(TableName.Organization)) + .select(db.ref("name").withSchema(TableName.Project).as("projectName")) + .select(db.ref("id").withSchema(TableName.Project).as("projectId")) + .select(db.ref("slug").withSchema(TableName.Project).as("projectSlug")) + .select(db.ref("createdAt").withSchema(TableName.Project).as("projectCreatedAt")) + .select(db.ref("email").withSchema(TableName.Users).as("userEmail")) + .select(db.ref("username").withSchema(TableName.Users).as("username")) + .select(db.ref("firstName").withSchema(TableName.Users).as("firstName")) + .select(db.ref("lastName").withSchema(TableName.Users).as("lastName")) + .select(db.ref("id").withSchema(TableName.Users).as("userId")) + .select(db.ref("id").withSchema(TableName.Membership).as("orgMembershipId")) + .select(db.ref("status").withSchema(TableName.Membership).as("orgMembershipStatus")) + .select(db.ref("role").withSchema(TableName.MembershipRole).as("orgMembershipRole")) + .select(db.ref("customRoleId").withSchema(TableName.MembershipRole).as("orgMembershipRoleId")) + .select(db.ref("name").withSchema(TableName.Role).as("orgMembershipRoleName")), + buildBaseQuery(countQuery).countDistinct(`${TableName.Organization}.id`, { as: "count" }).first() + ]); const formattedDocs = sqlNestRelationships({ data: docs, @@ -132,7 +142,12 @@ export const orgDALFactory = (db: TDbClient) => { ] }); - return formattedDocs; + const total = Number(totalResult?.count || 0); + + return { + organizations: formattedDocs, + total + }; } catch (error) { throw new DatabaseError({ error, name: "Find organizations by filter" }); } @@ -218,9 +233,12 @@ export const orgDALFactory = (db: TDbClient) => { ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[]> => { try { const org = (await db - .replicaNode()(TableName.OrgMembership) - .where({ userId }) - .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.actorUserId`, userId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -236,8 +254,8 @@ export const orgDALFactory = (db: TDbClient) => { ); }) .select(selectAllTableCols(TableName.Organization)) - .select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole")) - .select(db.ref("status").withSchema(TableName.OrgMembership).as("userStatus")) + .select(db.ref("role").withSchema(TableName.MembershipRole).as("userRole")) + .select(db.ref("status").withSchema(TableName.Membership).as("userStatus")) .select( db.raw(` CASE @@ -272,24 +290,28 @@ export const orgDALFactory = (db: TDbClient) => { const findAllOrgMembers = async (orgId: string) => { try { const members = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("inviteEmail").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("isActive").withSchema(TableName.OrgMembership), - db.ref("lastLoginAuthMethod").withSchema(TableName.OrgMembership), - db.ref("lastLoginTime").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("inviteEmail").withSchema(TableName.Membership), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("status").withSchema(TableName.Membership), + db.ref("isActive").withSchema(TableName.Membership), + db.ref("lastLoginAuthMethod").withSchema(TableName.Membership), + db.ref("lastLoginTime").withSchema(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), @@ -321,11 +343,13 @@ export const orgDALFactory = (db: TDbClient) => { } const count = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) .count("*") - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .where({ isGhost: false, [`${TableName.OrgMembership}.isActive` as "isActive"]: true }) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .where({ isGhost: false, [`${TableName.Membership}.isActive` as "isActive"]: true }) .first(); return parseInt((count as unknown as CountResult).count || "0", 10); @@ -337,21 +361,25 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => { try { const conn = tx || db.replicaNode(); - const members = await conn(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + const members = await conn(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - conn.ref("id").withSchema(TableName.OrgMembership), - conn.ref("inviteEmail").withSchema(TableName.OrgMembership), - conn.ref("orgId").withSchema(TableName.OrgMembership), - conn.ref("role").withSchema(TableName.OrgMembership), - conn.ref("roleId").withSchema(TableName.OrgMembership), - conn.ref("status").withSchema(TableName.OrgMembership), + conn.ref("id").withSchema(TableName.Membership), + conn.ref("inviteEmail").withSchema(TableName.Membership), + conn.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + conn.ref("status").withSchema(TableName.Membership), conn.ref("username").withSchema(TableName.Users), conn.ref("email").withSchema(TableName.Users), conn.ref("firstName").withSchema(TableName.Users), @@ -373,22 +401,25 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => { try { const conn = tx || db.replicaNode(); - const members = await conn(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .where(`${TableName.OrgMembership}.role`, role) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + const members = await conn(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .where(`${TableName.MembershipRole}.role`, role) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - conn.ref("id").withSchema(TableName.OrgMembership), - conn.ref("inviteEmail").withSchema(TableName.OrgMembership), - conn.ref("orgId").withSchema(TableName.OrgMembership), - conn.ref("role").withSchema(TableName.OrgMembership), - conn.ref("roleId").withSchema(TableName.OrgMembership), - conn.ref("status").withSchema(TableName.OrgMembership), + conn.ref("id").withSchema(TableName.Membership), + conn.ref("inviteEmail").withSchema(TableName.Membership), + conn.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + conn.ref("role").withSchema(TableName.MembershipRole), + conn.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + conn.ref("status").withSchema(TableName.Membership), conn.ref("username").withSchema(TableName.Users), conn.ref("email").withSchema(TableName.Users), conn.ref("firstName").withSchema(TableName.Users), @@ -407,47 +438,6 @@ export const orgDALFactory = (db: TDbClient) => { } }; - const findOrgGhostUser = async (orgId: string) => { - try { - 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`) - .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("email").withSchema(TableName.Users), - db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("publicKey").withSchema(TableName.UserEncryptionKey) - ) - .where({ isGhost: true }) - .first(); - return member; - } catch (error) { - return null; - } - }; - - const ghostUserExists = async (orgId: string) => { - try { - const member = await db - .replicaNode()(TableName.OrgMembership) - .where({ orgId }) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) - .select(db.ref("id").withSchema(TableName.Users).as("userId")) - .where({ isGhost: true }) - .first(); - return Boolean(member); - } catch (error) { - return false; - } - }; - const create = async (dto: TOrganizationsInsert, tx?: Knex) => { try { const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*"); @@ -480,38 +470,37 @@ export const orgDALFactory = (db: TDbClient) => { // MEMBERSHIP OPERATIONS // -------------------- - // const orgMembershipOrm = ormify(db, TableName.OrgMembership); - const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => { + const createMembership = async (data: TMembershipsInsert, tx?: Knex) => { try { - const [membership] = await (tx || db)(TableName.OrgMembership).insert(data).returning("*"); + const [membership] = await (tx || db)(TableName.Membership).insert(data).returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Create org membership" }); } }; - const bulkCreateMemberships = async (data: TOrgMembershipsInsert[], tx?: Knex) => { + const bulkCreateMemberships = async (data: TMembershipsInsert[], tx?: Knex) => { try { - const memberships = await (tx || db)(TableName.OrgMembership).insert(data).returning("*"); + const memberships = await (tx || db)(TableName.Membership).insert(data).returning("*"); return memberships; } catch (error) { throw new DatabaseError({ error, name: "Create org memberships" }); } }; - const updateMembershipById = async (id: string, data: TOrgMembershipsUpdate, tx?: Knex) => { + const updateMembershipById = async (id: string, data: TMembershipsUpdate, tx?: Knex) => { try { - const [membership] = await (tx || db)(TableName.OrgMembership).where({ id }).update(data).returning("*"); + const [membership] = await (tx || db)(TableName.Membership).where({ id }).update(data).returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Update org membership" }); } }; - const updateMembership = async (filter: Partial, data: TOrgMembershipsUpdate, tx?: Knex) => { + const updateMembership = async (filter: Partial, data: TMembershipsUpdate, tx?: Knex) => { try { - const membership = await (tx || db)(TableName.OrgMembership).where(filter).update(data).returning("*"); + const membership = await (tx || db)(TableName.Membership).where(filter).update(data).returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Update org memberships" }); @@ -520,7 +509,10 @@ export const orgDALFactory = (db: TDbClient) => { const deleteMembershipById = async (id: string, orgId: string, tx?: Knex) => { try { - const [membership] = await (tx || db)(TableName.OrgMembership).where({ id, orgId }).delete().returning("*"); + const [membership] = await (tx || db)(TableName.Membership) + .where({ id, scopeOrgId: orgId, scope: AccessScope.Organization }) + .delete() + .returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Delete org membership" }); @@ -529,9 +521,10 @@ export const orgDALFactory = (db: TDbClient) => { const deleteMembershipsById = async (ids: string[], orgId: string, tx?: Knex) => { try { - const memberships = await (tx || db)(TableName.OrgMembership) + const memberships = await (tx || db)(TableName.Membership) .where({ - orgId + scopeOrgId: orgId, + scope: AccessScope.Organization }) .whereIn("id", ids) .delete() @@ -543,22 +536,23 @@ export const orgDALFactory = (db: TDbClient) => { }; const findMembership = async ( - filter: TFindFilter, - { offset, limit, sort, tx }: TFindOpt = {} + filter: TFindFilter, + { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db.replicaNode())(TableName.OrgMembership) + const query = (tx || db.replicaNode())(TableName.Membership) // eslint-disable-next-line .where(buildFindFilter(filter)) - .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) - .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .where("scope", AccessScope.Organization) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { - this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`) - .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`) + this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) + .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) .andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"])); }) .select( - selectAllTableCols(TableName.OrgMembership), + selectAllTableCols(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), @@ -584,18 +578,20 @@ export const orgDALFactory = (db: TDbClient) => { const findMembershipWithScimFilter = async ( orgId: string, scimFilter: string | undefined, - { offset, limit, sort, tx }: TFindOpt = {} + { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db.replicaNode())(TableName.OrgMembership) + const query = (tx || db.replicaNode())(TableName.Membership) // eslint-disable-next-line - .where(`${TableName.OrgMembership}.orgId`, orgId) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) .where((qb) => { if (scimFilter) { void generateKnexQueryFromScim(qb, scimFilter, (attrPath) => { switch (attrPath) { case "active": - return `${TableName.OrgMembership}.isActive`; + return `${TableName.Membership}.isActive`; case "userName": return `${TableName.UserAliases}.externalId`; case "name.givenName": @@ -610,15 +606,15 @@ export const orgDALFactory = (db: TDbClient) => { }); } }) - .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) - .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { - this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`) - .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`) + this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) + .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) .andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"])); }) .select( - selectAllTableCols(TableName.OrgMembership), + selectAllTableCols(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), @@ -647,13 +643,16 @@ export const orgDALFactory = (db: TDbClient) => { ): Promise<{ id: string; name: string; slug: string; role: string }> => { try { const org = await db - .replicaNode()(TableName.IdentityOrgMembership) - .where({ identityId }) - .join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`) + .replicaNode()(TableName.Membership) + .where({ actorIdentityId: identityId }) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) .select(db.ref("id").withSchema(TableName.Organization).as("id")) .select(db.ref("name").withSchema(TableName.Organization).as("name")) .select(db.ref("slug").withSchema(TableName.Organization).as("slug")) - .select(db.ref("role").withSchema(TableName.IdentityOrgMembership).as("role")); + .select(db.ref("role").withSchema(TableName.MembershipRole).as("role")); return org?.[0]; } catch (error) { @@ -670,10 +669,8 @@ export const orgDALFactory = (db: TDbClient) => { findOrgBySlug, findAllOrgsByUserId, findOrganizationsByFilter, - ghostUserExists, findOrgMembersByUsername, findOrgMembersByRole, - findOrgGhostUser, create, updateById, deleteById, diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index 30e67cca4c..78d52e8168 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -1,129 +1,66 @@ +import { AccessScope } from "@app/db/schemas"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { BadRequestError } from "@app/lib/errors"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; -type TDeleteOrgMembership = { - orgMembershipId: string; - orgId: string; - orgDAL: Pick; - projectMembershipDAL: Pick; - projectKeyDAL: Pick; - userAliasDAL: Pick; - licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; - userId?: string; -}; +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; type TDeleteOrgMemberships = { orgMembershipIds: string[]; orgId: string; - orgDAL: Pick; - projectMembershipDAL: Pick; + orgDAL: Pick; + userGroupMembershipDAL: Pick; + membershipUserDAL: Pick; + membershipRoleDAL: Pick; projectKeyDAL: Pick; userAliasDAL: Pick; licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; userId?: string; -}; - -export const deleteOrgMembershipFn = async ({ - orgMembershipId, - orgId, - orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, - projectKeyDAL, - userAliasDAL, - licenseService, - userId -}: TDeleteOrgMembership) => { - const deletedMembership = await orgDAL.transaction(async (tx) => { - const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); - - if (userId && orgMembership.userId === userId) { - // scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken - throw new BadRequestError({ message: "You cannot remove yourself from an organization" }); - } - - if (!orgMembership.userId) { - await licenseService.updateSubscriptionOrgMemberCount(orgId); - return orgMembership; - } - - await userAliasDAL.delete( - { - userId: orgMembership.userId, - orgId - }, - tx - ); - - await projectUserAdditionalPrivilegeDAL.delete( - { - userId: orgMembership.userId - }, - tx - ); - - // Get all the project memberships of the user in the organization - const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId); - - // Delete all the project memberships of the user in the organization - await projectMembershipDAL.delete( - { - $in: { - id: projectMemberships.map((membership) => membership.id) - } - }, - tx - ); - - // Get all the project keys of the user in the organization - const projectKeys = await projectKeyDAL.find({ - $in: { - projectId: projectMemberships.map((membership) => membership.projectId) - }, - receiverId: orgMembership.userId - }); - - // Delete all the project keys of the user in the organization - await projectKeyDAL.delete( - { - $in: { - id: projectKeys.map((key) => key.id) - } - }, - tx - ); - - await licenseService.updateSubscriptionOrgMemberCount(orgId); - return orgMembership; - }); - - return deletedMembership; + additionalPrivilegeDAL: Pick; }; export const deleteOrgMembershipsFn = async ({ orgMembershipIds, orgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL, + userGroupMembershipDAL, + membershipRoleDAL, + additionalPrivilegeDAL }: TDeleteOrgMemberships) => { const deletedMemberships = await orgDAL.transaction(async (tx) => { - const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx); + await membershipRoleDAL.delete( + { + $in: { + membershipId: orgMembershipIds + } + }, + tx + ); + + const orgMemberships = await membershipUserDAL.delete( + { + scopeOrgId: orgId, + scope: AccessScope.Organization, + $in: { + id: orgMembershipIds + } + }, + tx + ); const membershipUserIds = orgMemberships - .filter((member) => Boolean(member.userId)) - .map((member) => member.userId) as string[]; + .filter((member) => Boolean(member.actorUserId)) + .map((member) => member.actorUserId) as string[]; if (userId && membershipUserIds.includes(userId)) { // scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken @@ -145,41 +82,55 @@ export const deleteOrgMembershipsFn = async ({ tx ); - await projectUserAdditionalPrivilegeDAL.delete( - { - $in: { - userId: membershipUserIds - } - }, - tx - ); - // Get all the project memberships of the users in the organization - const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, membershipUserIds); // Delete all the project memberships of the users in the organization - await projectMembershipDAL.delete( + const otherMemberships = await membershipUserDAL.delete( { + scopeOrgId: orgId, $in: { - id: projectMemberships.map((membership) => membership.id) + actorUserId: membershipUserIds } }, tx ); - // Get all the project keys of the user in the organization - const projectKeys = await projectKeyDAL.find({ - $in: { - projectId: projectMemberships.map((membership) => membership.projectId), - receiverId: membershipUserIds - } + const orgGroups = await membershipUserDAL.find({ + scopeOrgId: orgId, + $notNull: ["actorGroupId"] }); + const groupIds = orgGroups.filter((el) => el.actorGroupId).map((el) => el.actorGroupId as string); + + await userGroupMembershipDAL.delete( + { + $in: { + userId: membershipUserIds, + groupId: groupIds + } + }, + tx + ); + const projectIds = otherMemberships + .filter((el) => el.scope === AccessScope.Project && el.scopeProjectId) + .map((el) => el.scopeProjectId as string); + + await additionalPrivilegeDAL.delete( + { + $in: { + projectId: projectIds, + actorUserId: membershipUserIds + } + }, + tx + ); + // Delete all the project keys of the user in the organization await projectKeyDAL.delete( { $in: { - id: projectKeys.map((key) => key.id) + projectId: projectIds, + receiverId: membershipUserIds } }, tx diff --git a/backend/src/services/org/org-role-dal.ts b/backend/src/services/org/org-role-dal.ts deleted file mode 100644 index 2bc57001ab..0000000000 --- a/backend/src/services/org/org-role-dal.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TOrgRoleDALFactory = ReturnType; - -export const orgRoleDALFactory = (db: TDbClient) => ormify(db, TableName.OrgRoles); diff --git a/backend/src/services/org/org-role-fns.ts b/backend/src/services/org/org-role-fns.ts index 5bff1e324a..96e01b4052 100644 --- a/backend/src/services/org/org-role-fns.ts +++ b/backend/src/services/org/org-role-fns.ts @@ -1,7 +1,8 @@ import { OrgMembershipRole } from "@app/db/schemas"; import { TFeatureSet } from "@app/ee/services/license/license-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; + +import { TRoleDALFactory } from "../role/role-dal"; const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom"); @@ -10,13 +11,13 @@ export const isCustomOrgRole = (roleSlug: string) => !RESERVED_ORG_ROLE_SLUGS.fi // this is only for updating an org export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ membershipRoleSlug, - orgRoleDAL, + roleDAL, plan, orgId }: { orgId: string; membershipRoleSlug: string; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; plan: TFeatureSet; }) => { if (isCustomOrgRole(membershipRoleSlug)) { @@ -26,7 +27,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ "Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role." }); - const customRole = await orgRoleDAL.findOne({ slug: membershipRoleSlug, orgId }); + const customRole = await roleDAL.findOne({ slug: membershipRoleSlug, orgId }); if (!customRole) { throw new NotFoundError({ name: "UpdateOrg", diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts deleted file mode 100644 index 6ce2b22cd0..0000000000 --- a/backend/src/services/org/org-role-service.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { ForbiddenError } from "@casl/ability"; -import { packRules } from "@casl/ability/extra"; - -import { TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas"; -import { - orgAdminPermissions, - orgMemberPermissions, - orgNoAccessPermissions, - OrgPermissionActions, - OrgPermissionSubjects -} from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; -import { TOrgDALFactory } from "@app/services/org/org-dal"; - -import { ActorAuthMethod } from "../auth/auth-type"; -import { TOrgRoleDALFactory } from "./org-role-dal"; - -type TOrgRoleServiceFactoryDep = { - orgRoleDAL: TOrgRoleDALFactory; - permissionService: TPermissionServiceFactory; - orgDAL: TOrgDALFactory; - externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; -}; - -export type TOrgRoleServiceFactory = ReturnType; - -export const orgRoleServiceFactory = ({ - orgRoleDAL, - orgDAL, - permissionService, - externalGroupOrgRoleMappingDAL -}: TOrgRoleServiceFactoryDep) => { - const createRole = async ( - userId: string, - orgId: string, - data: Omit, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role); - const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId }); - if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" }); - const role = await orgRoleDAL.create({ - ...data, - orgId, - permissions: JSON.stringify(data.permissions) - }); - return role; - }; - - const getRole = async ( - userId: string, - orgId: string, - roleId: string, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); - - switch (roleId) { - case "b11b49a9-09a9-4443-916a-4246f9ff2c69": { - return { - id: roleId, - orgId, - name: "Admin", - slug: "admin", - description: "Complete administration access over the organization", - permissions: packRules(orgAdminPermissions), - createdAt: new Date(), - updatedAt: new Date() - }; - } - case "b11b49a9-09a9-4443-916a-4246f9ff2c70": { - return { - id: roleId, - orgId, - name: "Member", - slug: "member", - description: "Non-administrative role in an organization", - permissions: packRules(orgMemberPermissions), - createdAt: new Date(), - updatedAt: new Date() - }; - } - case "b10d49a9-09a9-4443-916a-4246f9ff2c72": { - return { - id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response - orgId, - name: "No Access", - slug: "no-access", - description: "No access to any resources in the organization", - permissions: packRules(orgNoAccessPermissions), - createdAt: new Date(), - updatedAt: new Date() - }; - } - default: { - const role = await orgRoleDAL.findOne({ id: roleId, orgId }); - if (!role) throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found` }); - return role; - } - } - }; - - const updateRole = async ( - userId: string, - orgId: string, - roleId: string, - data: Omit, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); - if (data?.slug) { - const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId }); - if (existingRole && existingRole.id !== roleId) - throw new BadRequestError({ name: "Update Role", message: "Duplicate role" }); - } - const [updatedRole] = await orgRoleDAL.update( - { id: roleId, orgId }, - { ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined } - ); - if (!updatedRole) throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found` }); - return updatedRole; - }; - - const deleteRole = async ( - userId: string, - orgId: string, - roleId: string, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); - - const org = await orgDAL.findOrgById(orgId); - - if (!org) - throw new NotFoundError({ - message: `Organization with ID '${orgId}' not found` - }); - - if (org.defaultMembershipRole === roleId) - throw new BadRequestError({ - message: "Cannot delete default org membership role. Please re-assign and try again." - }); - - const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ - orgId, - roleId - }); - - if (externalGroupMapping) - throw new BadRequestError({ - message: - "Cannot delete role assigned to external group organization role mapping. Please re-assign external mapping and try again." - }); - - const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId }); - if (!deletedRole) - throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found`, name: "UpdateRole" }); - - return deletedRole; - }; - - const listRoles = async ( - userId: string, - orgId: string, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); - const customRoles = await orgRoleDAL.find({ orgId }); - const roles = [ - { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid - orgId, - name: "Admin", - slug: "admin", - description: "Complete administration access over the organization", - permissions: packRules(orgAdminPermissions), - createdAt: new Date(), - updatedAt: new Date() - }, - { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response - orgId, - name: "Member", - slug: "member", - description: "Non-administrative role in an organization", - permissions: packRules(orgMemberPermissions), - createdAt: new Date(), - updatedAt: new Date() - }, - { - id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response - orgId, - name: "No Access", - slug: "no-access", - description: "No access to any resources in the organization", - permissions: packRules(orgNoAccessPermissions), - createdAt: new Date(), - updatedAt: new Date() - }, - ...(customRoles || []).map(({ permissions, ...data }) => ({ - ...data, - permissions - })) - ]; - - return roles; - }; - - const getUserPermission = async ( - userId: string, - orgId: string, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { - const { permission, membership } = await permissionService.getUserOrgPermission( - userId, - orgId, - actorAuthMethod, - actorOrgId - ); - return { permissions: packRules(permission.rules), membership }; - }; - - return { createRole, getRole, updateRole, deleteRole, listRoles, getUserPermission }; -}; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index ffcf4459ee..5b98b44b18 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -3,19 +3,15 @@ import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; import { - ActionProjectType, + AccessScope, OrgMembershipRole, OrgMembershipStatus, - ProjectMembershipRole, - ProjectVersion, TableName, TOidcConfigs, - TProjectMemberships, - TProjectUserMembershipRolesInsert, - TSamlConfigs, - TUsers + TSamlConfigs } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLdapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; @@ -25,47 +21,35 @@ import { OrgPermissionSecretShareAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { - constructPermissionErrorMessage, - validatePrivilegeChangeOperation -} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; -import { - BadRequestError, - ForbiddenRequestError, - NotFoundError, - PermissionBoundaryError, - UnauthorizedError -} from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { isDisposableEmail } from "@app/lib/validator"; import { QueueName } from "@app/queue"; import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { ActorAuthMethod, ActorType, AuthMethod, AuthModeJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TReminderServiceFactory } from "../reminder/reminder-types"; +import { TRoleDALFactory } from "../role/role-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -75,8 +59,7 @@ import { TUserDALFactory } from "../user/user-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TOrgBotDALFactory } from "./org-bot-dal"; import { TOrgDALFactory } from "./org-dal"; -import { deleteOrgMembershipFn, deleteOrgMembershipsFn } from "./org-fns"; -import { TOrgRoleDALFactory } from "./org-role-dal"; +import { deleteOrgMembershipsFn } from "./org-fns"; import { TDeleteOrgMembershipDTO, TDeleteOrgMembershipsDTO, @@ -84,7 +67,6 @@ import { TFindOrgMembersByEmailDTO, TGetOrgGroupsDTO, TGetOrgMembershipDTO, - TInviteUserToOrgDTO, TListProjectMembershipsByOrgMembershipIdDTO, TResendOrgMemberInvitationDTO, TUpdateOrgDTO, @@ -100,31 +82,22 @@ type TOrgServiceFactoryDep = { folderDAL: Pick; orgDAL: TOrgDALFactory; orgBotDAL: TOrgBotDALFactory; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; userDAL: TUserDALFactory; groupDAL: TGroupDALFactory; projectDAL: TProjectDALFactory; identityMetadataDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; projectMembershipDAL: Pick< TProjectMembershipDALFactory, - | "findProjectMembershipsByUserId" - | "delete" - | "create" - | "find" - | "insertMany" - | "transaction" - | "findProjectMembershipsByUserIds" + "findProjectMembershipsByUserId" | "findProjectMembershipsByUserIds" >; projectKeyDAL: Pick; orgMembershipDAL: Pick< TOrgMembershipDALFactory, - | "findOrgMembershipById" - | "findOne" - | "findById" - | "findRecentInvitedMemberships" - | "updateById" - | "updateLastInvitedAtByIds" + "findOrgMembershipById" | "findRecentInvitedMemberships" | "updateLastInvitedAtByIds" >; + membershipRoleDAL: TMembershipRoleDALFactory; incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; oidcConfigDAL: Pick; @@ -136,12 +109,11 @@ type TOrgServiceFactoryDep = { TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer" >; - projectUserAdditionalPrivilegeDAL: Pick; - projectRoleDAL: Pick; - projectUserMembershipRoleDAL: Pick; projectBotService: Pick; loginService: Pick; reminderService: Pick; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; }; export type TOrgServiceFactory = ReturnType; @@ -154,7 +126,7 @@ export const orgServiceFactory = ({ folderDAL, userDAL, groupDAL, - orgRoleDAL, + roleDAL, incidentContactDAL, permissionService, smtpService, @@ -162,19 +134,20 @@ export const orgServiceFactory = ({ projectMembershipDAL, projectKeyDAL, orgMembershipDAL, - projectUserAdditionalPrivilegeDAL, tokenService, orgBotDAL, licenseService, - projectRoleDAL, samlConfigDAL, oidcConfigDAL, ldapConfigDAL, - projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, loginService, - reminderService + reminderService, + membershipRoleDAL, + membershipUserDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id @@ -185,7 +158,7 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission(ActorType.USER, userId, orgId, actorAuthMethod, actorOrgId); const appCfg = getConfig(); const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); @@ -221,7 +194,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findAllOrgMembers(orgId); @@ -267,7 +246,7 @@ export const orgServiceFactory = ({ } if (actor === ActorType.IDENTITY) { - const workspaces = await projectDAL.findAllProjectsByIdentity(actorId); + const workspaces = await projectDAL.findIdentityProjects(actorId, orgId); return workspaces; } @@ -302,14 +281,21 @@ export const orgServiceFactory = ({ ); const createMembershipData = { - orgId, - userId: user.id, - role: OrgMembershipRole.Admin, + scopeOrgId: orgId, + scope: AccessScope.Organization, + actorUserId: user.id, status: OrgMembershipStatus.Accepted, isActive: true }; - await orgDAL.createMembership(createMembershipData, tx); + const membership = await orgDAL.createMembership(createMembershipData, tx); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); return { user, @@ -323,9 +309,15 @@ export const orgServiceFactory = ({ actorAuthMethod, orgId }: TUpgradePrivilegeSystemDTO) => { - const { membership } = await permissionService.getUserOrgPermission(actorId, orgId, actorAuthMethod, actorOrgId); + const { hasRole } = await permissionService.getOrgPermission( + ActorType.USER, + actorId, + orgId, + actorAuthMethod, + actorOrgId + ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Insufficient privileges - only the organization admin can upgrade the privilege system." }); @@ -531,7 +523,7 @@ export const orgServiceFactory = ({ defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({ membershipRoleSlug: defaultMembershipRoleSlug, orgId, - orgRoleDAL, + roleDAL, plan }); } @@ -601,16 +593,23 @@ export const orgServiceFactory = ({ tx ); if (userId) { - await orgDAL.createMembership( + const membership = await orgDAL.createMembership( { - userId, - orgId: org.id, - role: OrgMembershipRole.Admin, + scope: AccessScope.Organization, + actorUserId: userId, + scopeOrgId: org.id, status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); } await orgBotDAL.create( { @@ -659,8 +658,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string | undefined; }) => { - const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) { + const { hasRole } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ name: "DeleteOrganizationById", message: "Insufficient privileges" @@ -739,22 +744,32 @@ export const orgServiceFactory = ({ actorOrgId, metadata }: TUpdateOrgMembershipDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); - const foundMembership = await orgMembershipDAL.findById(membershipId); + const foundMembership = await membershipUserDAL.findOne({ + id: membershipId, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }); if (!foundMembership) throw new NotFoundError({ message: `Organization membership with ID ${membershipId} not found` }); - if (foundMembership.orgId !== orgId) + if (foundMembership.scopeOrgId !== orgId) throw new UnauthorizedError({ message: "Updated org member doesn't belong to the organization" }); - if (foundMembership.userId === userId) + if (foundMembership.actorUserId === userId) throw new UnauthorizedError({ message: "Cannot update own organization membership" }); const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); let userRole = role; let userRoleId: string | null = null; if (role && isCustomRole) { - const customRole = await orgRoleDAL.findOne({ slug: role, orgId }); + const customRole = await roleDAL.findOne({ slug: role, orgId }); if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" }); const plan = await licenseService.getPlan(orgId); @@ -767,17 +782,33 @@ export const orgServiceFactory = ({ userRoleId = customRole.id; } const membership = await orgDAL.transaction(async (tx) => { - const [updatedOrgMembership] = await orgDAL.updateMembership( - { id: membershipId, orgId }, - { role: userRole, roleId: userRoleId, isActive } - ); + // this is because if isActive is undefined then this would fail due to knexjs error + const [updatedOrgMembership] = + typeof isActive === "undefined" + ? [foundMembership] + : await orgDAL.updateMembership( + { id: membershipId, scopeOrgId: orgId, scope: AccessScope.Organization }, + { isActive }, + tx + ); + if (userRole) { + await membershipRoleDAL.delete({ membershipId }, tx); + await membershipRoleDAL.create( + { + membershipId, + role: userRole, + customRoleId: userRoleId + }, + tx + ); + } if (metadata) { - await identityMetadataDAL.delete({ userId: updatedOrgMembership.userId, orgId }, tx); + await identityMetadataDAL.delete({ userId: updatedOrgMembership.actorUserId, orgId }, tx); if (metadata.length) { await identityMetadataDAL.insertMany( metadata.map(({ key, value }) => ({ - userId: updatedOrgMembership.userId, + userId: updatedOrgMembership.actorUserId as string, orgId, key, value @@ -809,8 +840,9 @@ export const orgServiceFactory = ({ const org = await orgDAL.findOrgById(orgId); const [inviteeOrgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, - [`${TableName.OrgMembership}.id` as "id"]: membershipId + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, + [`${TableName.Membership}.id` as "id"]: membershipId }); if (inviteeOrgMembership.status !== OrgMembershipStatus.Invited) { @@ -821,7 +853,7 @@ export const orgServiceFactory = ({ const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: inviteeOrgMembership.userId, + userId: inviteeOrgMembership.actorUserId as string, orgId }); @@ -849,366 +881,13 @@ export const orgServiceFactory = ({ } }); - await orgMembershipDAL.updateById(inviteeOrgMembership.id, { + await membershipUserDAL.updateById(inviteeOrgMembership.id, { lastInvitedAt: new Date() }); return { signupToken: undefined }; }; - /* - * Invite user to organization - */ - const inviteUserToOrganization = async ({ - orgId, - actorId, - actor, - inviteeEmails, - organizationRoleSlug, - projects: invitedProjects, - actorAuthMethod, - actorOrgId - }: TInviteUserToOrgDTO) => { - const appCfg = getConfig(); - - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - - const invitingUser = await userDAL.findOne({ id: actorId }); - - const org = await orgDAL.findOrgById(orgId); - - const isEmailInvalid = await isDisposableEmail(inviteeEmails); - if (isEmailInvalid) { - throw new BadRequestError({ - message: "Disposable emails are not allowed", - name: "InviteUser" - }); - } - const plan = await licenseService.getPlan(orgId); - const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole); - if (isCustomOrgRole) { - if (!plan?.rbac) - throw new BadRequestError({ - message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member." - }); - } - - const projectsToInvite = invitedProjects?.length - ? await projectDAL.find({ - orgId, - $in: { - id: invitedProjects?.map(({ id }) => id) - } - }) - : []; - - if (projectsToInvite.length !== invitedProjects?.length) { - throw new ForbiddenRequestError({ - message: "Access denied to one or more of the specified projects" - }); - } - - if (projectsToInvite.some((el) => el.version !== ProjectVersion.V3)) { - throw new BadRequestError({ - message: "One or more selected projects are not compatible with this operation. Please upgrade your projects." - }); - } - - const mailsForOrgInvitation: { email: string; userId: string; firstName: string; lastName: string }[] = []; - const mailsForProjectInvitation: { email: string[]; projectName: string }[] = []; - const newProjectMemberships: TProjectMemberships[] = []; - - await orgDAL.transaction(async (tx) => { - const users: Pick[] = []; - - for await (const inviteeEmail of inviteeEmails) { - const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx); - let inviteeUser = - usersByUsername?.length > 1 - ? usersByUsername.find((el) => el.username === inviteeEmail) - : usersByUsername?.[0]; - - // if the user doesn't exist we create the user with the email - if (!inviteeUser) { - // TODO(carlos): will be removed once the function receives usernames instead of emails - const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx); - if (usersByEmail?.length === 1) { - [inviteeUser] = usersByEmail; - } else { - inviteeUser = await userDAL.create( - { - isAccepted: false, - email: inviteeEmail, - username: inviteeEmail, - authMethods: [AuthMethod.EMAIL], - isGhost: false - }, - tx - ); - } - } - - const inviteeUserId = inviteeUser?.id; - const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx); - - // when user is missing the encrytion keys - // this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp - // So what we do is we generate a random secure password and then encrypt it with a random pub-private key - // Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password - if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) { - await userDAL.createUserEncryption( - { - userId: inviteeUserId, - encryptionVersion: 2 - }, - tx - ); - } - - const [inviteeOrgMembership] = await orgDAL.findMembership( - { - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, - [`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId - }, - { tx } - ); - - // if there exist no org membership we set is as given by the request - if (!inviteeOrgMembership) { - if (plan?.slug !== "enterprise" && 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({ - name: "InviteUser", - message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." - }); - } - - if (org?.authEnforced) { - throw new ForbiddenRequestError({ - name: "InviteUser", - message: "Failed to invite user due to org-level auth enforced for organization" - }); - } - - // as its used by project invite also - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); - let roleId; - const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug; - if (isCustomOrgRole) { - const customRole = await orgRoleDAL.findOne({ slug: organizationRoleSlug, orgId }); - if (!customRole) { - throw new NotFoundError({ - name: "InviteUser", - message: `Custom organization role with slug '${orgRole}' not found` - }); - } - roleId = customRole.id; - } - - await orgDAL.createMembership( - { - userId: inviteeUser.id, - inviteEmail: inviteeEmail, - orgId, - role: orgRole, - status: OrgMembershipStatus.Invited, - isActive: true, - roleId - }, - tx - ); - mailsForOrgInvitation.push({ - email: inviteeEmail, - userId: inviteeUser.id, - firstName: inviteeUser?.firstName || "", - lastName: inviteeUser.lastName || "" - }); - } - - users.push(inviteeUser); - } - - const userIds = users.map(({ id }) => id); - const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx); - // we don't need to spam with email. Thus org invitation doesn't need project invitation again - const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId)); - - // if there exist no project membership we set is as given by the request - for await (const project of projectsToInvite) { - const projectId = project.id; - const { permission: projectPermission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionMemberActions.Create, - ProjectPermissionSub.Member - ); - const existingMembers = await projectMembershipDAL.find( - { - projectId: project.id, - $in: { userId: userIds } - }, - { tx } - ); - const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId); - const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter( - (user) => !existingMembersGroupByUserId?.[user.userId] - ); - - // eslint-disable-next-line no-continue - if (!userWithEncryptionKeyInvitedToProject.length) continue; - - // validate custom project role - const invitedProjectRoles = invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || [ - ProjectMembershipRole.Member - ]; - - for await (const invitedRole of invitedProjectRoles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - invitedRole, - projectId - ); - - if (invitedRole !== ProjectMembershipRole.NoAccess) { - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - projectPermission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to invite user to the project", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - } - - const customProjectRoles = invitedProjectRoles.filter( - (role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customProjectRoles.length); - if (hasCustomRole) { - if (!plan?.rbac) - throw new BadRequestError({ - name: "InviteUser", - message: - "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member." - }); - } - - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId, - $in: { slug: customProjectRoles.map((role) => role) } - }) - : []; - if (customRoles.length !== customProjectRoles.length) { - throw new NotFoundError({ name: "InviteUser", message: "Custom project role not found" }); - } - - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const projectMemberships = await projectMembershipDAL.insertMany( - userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ - projectId, - userId: userEnc.userId - })), - tx - ); - newProjectMemberships.push(...projectMemberships); - - const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = []; - invitedProjectRoles.forEach((projectRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]); - projectMemberships.forEach((membershipEntry) => { - sanitizedProjectMembershipRoles.push({ - projectMembershipId: membershipEntry.id, - role: isCustomRole ? ProjectMembershipRole.Custom : projectRole, - customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null - }); - }); - }); - await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - - mailsForProjectInvitation.push({ - email: userWithEncryptionKeyInvitedToProject - .filter((el) => !userIdsWithOrgInvitation.has(el.userId)) - .map((el) => el.email || el.username), - projectName: project.name - }); - } - return users; - }); - - await licenseService.updateSubscriptionOrgMemberCount(orgId); - const signupTokens: { email: string; link: string }[] = []; - // send org invite mail - await Promise.allSettled( - mailsForOrgInvitation.map(async (el) => { - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: el.userId, - orgId - }); - - signupTokens.push({ - email: el.email, - link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${org?.id}` - }); - - return smtpService.sendMail({ - template: SmtpTemplates.OrgInvite, - subjectLine: "Infisical organization invitation", - recipients: [el.email], - substitutions: { - inviterFirstName: invitingUser?.firstName, - inviterUsername: invitingUser?.email, - organizationName: org?.name, - email: el.email, - organizationId: org?.id.toString(), - token, - callback_url: `${appCfg.SITE_URL}/signupinvite` - } - }); - }) - ); - - await Promise.allSettled( - mailsForProjectInvitation - .filter((el) => Boolean(el.email.length)) - .map(async (el) => { - return smtpService.sendMail({ - template: SmtpTemplates.WorkspaceInvite, - subjectLine: "Infisical project invitation", - recipients: el.email, - substitutions: { - workspaceName: el.projectName, - callback_url: `${appCfg.SITE_URL}/login` - } - }); - }) - ); - - if (!appCfg.isSmtpConfigured) { - return { signupTokens, projectMemberships: newProjectMemberships }; - } - - return { signupTokens: undefined, projectMemberships: newProjectMemberships }; - }; - /** * Organization invitation step 2: Verify that code [code] was sent to email [email] as part of * magic link and issue a temporary signup token for user to complete setting up their account @@ -1222,9 +901,10 @@ export const orgServiceFactory = ({ } const [orgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + scope: AccessScope.Organization, status: OrgMembershipStatus.Invited, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId }); if (!orgMembership) @@ -1237,7 +917,7 @@ export const orgServiceFactory = ({ await tokenService.validateTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, userId: user.id, - orgId: orgMembership.orgId, + orgId: orgMembership.scopeOrgId, code }); @@ -1249,16 +929,17 @@ export const orgServiceFactory = ({ // this means user has already completed signup process // isAccepted is set true when keys are exchanged await orgDAL.updateMembershipById(orgMembership.id, { - orgId, + scopeOrgId: orgId, status: OrgMembershipStatus.Accepted }); await licenseService.updateSubscriptionOrgMemberCount(orgId); return { user }; } + const membershipRole = await membershipRoleDAL.findOne({ membershipId: orgMembership.id }); if ( organization.authEnforced && - !(organization.bypassOrgAuthEnabled && orgMembership.role === OrgMembershipRole.Admin) + !(organization.bypassOrgAuthEnabled && membershipRole.role === OrgMembershipRole.Admin) ) { return { user }; } @@ -1307,19 +988,27 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); - const deletedMembership = await deleteOrgMembershipFn({ - orgMembershipId: membershipId, + const [deletedMembership] = await deleteOrgMembershipsFn({ + orgMembershipIds: [membershipId], orgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL, + membershipRoleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); return deletedMembership; @@ -1332,7 +1021,13 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipsDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); if (membershipIds.includes(userId)) { @@ -1343,12 +1038,14 @@ export const orgServiceFactory = ({ orgMembershipIds: membershipIds, orgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL, + membershipRoleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); return deletedMemberships; @@ -1385,7 +1082,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); const incidentContacts = await incidentContactDAL.findByOrgId(orgId); return incidentContacts; @@ -1398,7 +1101,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); if (doesIncidentContactExist) { @@ -1419,7 +1128,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); const incidentContact = await incidentContactDAL.deleteById(id, orgId); @@ -1440,17 +1155,17 @@ export const orgServiceFactory = ({ await Promise.all( invitedUsers.map(async (invitedUser) => { - let org = orgCache[invitedUser.orgId]; + let org = orgCache[invitedUser.scopeOrgId]; if (!org) { - org = await orgDAL.findById(invitedUser.orgId); - orgCache[invitedUser.orgId] = org; + org = await orgDAL.findById(invitedUser.scopeOrgId); + orgCache[invitedUser.scopeOrgId] = org; } - if (!org || !invitedUser.userId) return; + if (!org || !invitedUser.actorUserId) return; const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: invitedUser.userId, + userId: invitedUser.actorUserId, orgId: org.id }); @@ -1488,7 +1203,6 @@ export const orgServiceFactory = ({ findAllOrgMembers, findAllOrganizationOfUser, findIdentityOrganization, - inviteUserToOrganization, verifyUserToOrg, updateOrg, findOrgMembersByUsername, diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-constants.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-constants.ts new file mode 100644 index 0000000000..265e00cac0 --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-constants.ts @@ -0,0 +1,52 @@ +import RE2 from "re2"; + +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +/** + * AWS Certificate Manager naming constraints for certificates + */ +export const AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING = { + /** + * Regular expression pattern for valid AWS Certificate Manager certificate names + * Must contain only alphanumeric characters, spaces, hyphens, and underscores + * Must be 1-256 characters long + */ + NAME_PATTERN: new RE2("^[a-zA-Z0-9\\s\\-_]{1,256}$"), + + /** + * String of characters that are forbidden in AWS Certificate Manager certificate names + */ + FORBIDDEN_CHARACTERS: "!@#$%^&*()+={}[]|\\:;\"'<>,.?/~`", + + /** + * Maximum length for certificate names in AWS Certificate Manager + */ + MAX_LENGTH: 256, + + /** + * Minimum length for certificate names in AWS Certificate Manager + */ + MIN_LENGTH: 1, + + /** + * String representation of the allowed character pattern (for UI display) + */ + ALLOWED_CHARACTER_PATTERN: "^[a-zA-Z0-9\\s\\-_]{1,256}$" +} as const; + +/** + * AWS Certificate Manager PKI Sync list option configuration + */ +export const AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION = { + name: "AWS Certificate Manager" as const, + connection: AppConnection.AWS, + destination: PkiSync.AwsCertificateManager, + canImportCertificates: false, + canRemoveCertificates: true, + defaultCertificateNameSchema: "Infisical-{{certificateId}}", + forbiddenCharacters: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS, + allowedCharacterPattern: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN, + maxCertificateNameLength: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MAX_LENGTH, + minCertificateNameLength: AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MIN_LENGTH +} as const; diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts new file mode 100644 index 0000000000..f78bd790e8 --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts @@ -0,0 +1,634 @@ +/* eslint-disable no-await-in-loop */ +import * as AWS from "aws-sdk"; +import RE2 from "re2"; +import { z } from "zod"; + +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { AwsConnectionMethod } from "@app/services/app-connection/aws/aws-connection-enums"; +import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; +import { + AwsConnectionAccessTokenCredentialsSchema, + AwsConnectionAssumeRoleCredentialsSchema +} from "@app/services/app-connection/aws/aws-connection-schemas"; +import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types"; +import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types"; + +import { PkiSyncError } from "../pki-sync-errors"; +import { TPkiSyncWithCredentials } from "../pki-sync-types"; +import { + ACMCertificateWithKey, + CertificateImportRequest, + RemoveCertificatesResult, + SyncCertificatesResult, + TAwsCertificateManagerPkiSyncConfig +} from "./aws-certificate-manager-pki-sync-types"; + +const INFISICAL_CERTIFICATE_TAG = "InfisicalCertificate"; +const AWS_CERTIFICATE_ARN_PATTERN = new RE2("^arn:aws:acm:[a-z0-9-]+:\\d{12}:certificate/[a-f0-9-]{36}$"); + +type TAwsAssumeRoleCredentials = z.infer; +type TAwsAccessKeyCredentials = z.infer; + +const AWS_RATE_LIMIT_CONFIG: RateLimitConfig = { + MAX_CONCURRENT_REQUESTS: 10, + BASE_DELAY: 1000, + MAX_DELAY: 30000, + MAX_RETRIES: 3, + RATE_LIMIT_STATUS_CODES: [429, 503] +}; + +const awsConnectionQueue = createConnectionQueue(AWS_RATE_LIMIT_CONFIG); + +const { withRateLimitRetry, executeWithConcurrencyLimit } = awsConnectionQueue; + +const validateCertificateArn = (arn: string): boolean => { + return AWS_CERTIFICATE_ARN_PATTERN.test(arn); +}; + +const extractCertificateNameFromArn = (certificateArn: string): string => { + if (!validateCertificateArn(certificateArn)) { + throw new Error(`Invalid AWS Certificate Manager ARN format: ${certificateArn}`); + } + const parts = certificateArn.split("/"); + return parts[parts.length - 1]; +}; + +const sanitizeInput = (input: string): string => { + return input.trim().replace(new RE2("[^\\w\\s-]", "g"), ""); +}; + +const validateCertificateContent = (cert: string, privateKey: string): void => { + if (!cert || cert.trim().length === 0) { + throw new Error("Certificate content is empty or missing"); + } + + if (!privateKey || privateKey.trim().length === 0) { + throw new Error("Private key content is empty or missing"); + } + + if (!cert.includes("-----BEGIN CERTIFICATE-----") || !cert.includes("-----END CERTIFICATE-----")) { + throw new Error("Certificate is not in valid PEM format"); + } + + if (!privateKey.includes("-----BEGIN") || !privateKey.includes("-----END")) { + throw new Error("Private key is not in valid PEM format"); + } +}; + +const isAwsIssuedCertificate = (certificate: AWS.ACM.CertificateSummary): boolean => { + return certificate.Type === "AMAZON_ISSUED"; +}; + +const shouldSkipCertificateExport = (certificate: AWS.ACM.CertificateSummary): boolean => { + return isAwsIssuedCertificate(certificate); +}; + +const findTagByKey = (tags: AWS.ACM.TagList | undefined, key: string): AWS.ACM.Tag | undefined => { + if (!tags || !Array.isArray(tags)) { + return undefined; + } + return tags.find((tag: AWS.ACM.Tag) => tag.Key === key && tag.Value); +}; + +const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM.Tag | undefined => { + return findTagByKey(tags, INFISICAL_CERTIFICATE_TAG); +}; + +const validateCertificateIdentification = ( + certName: string, + existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string } +): boolean => { + if (!existingCert?.arn || !existingCert?.Tags) { + return false; + } + + const certNameTag = findInfisicalCertificateTag(existingCert.Tags); + + if (!certNameTag || !certNameTag.Value) { + return false; + } + + return certNameTag.Value === certName; +}; + +type TAwsCertificateManagerPkiSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const validateCertificateNameSchema = (schema: string): void => { + if (!schema.includes("{{certificateId}}")) { + throw new Error( + "Certificate name schema must include {{certificateId}} placeholder for proper certificate identification" + ); + } +}; + +const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithCredentials): string => { + if (!certificateName || typeof certificateName !== "string") { + throw new Error("Certificate name must be a non-empty string"); + } + + const sanitizedCertificateName = sanitizeInput(certificateName); + const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined; + const certificateNameSchema = syncOptions?.certificateNameSchema; + + if (certificateNameSchema) { + validateCertificateNameSchema(certificateNameSchema); + + let certificateId: string; + + if (sanitizedCertificateName.startsWith("Infisical-")) { + certificateId = sanitizedCertificateName.substring("Infisical-".length); + } else { + certificateId = sanitizedCertificateName; + } + + if (!certificateId || certificateId.trim().length === 0) { + throw new Error(`Certificate ID cannot be empty after processing certificate name: ${certificateName}`); + } + + const environment = "global"; + const generatedName = certificateNameSchema + .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), certificateId) + .replace(new RE2("\\{\\{environment\\}\\}", "g"), environment); + + if (generatedName.length > 256 || generatedName.length < 1) { + throw new Error( + `Generated certificate name length (${generatedName.length}) must be between 1 and 256 characters` + ); + } + + if (generatedName.includes("{{certificateId}}")) { + throw new Error("Certificate name schema failed to properly replace {{certificateId}} placeholder"); + } + + return generatedName; + } + + return sanitizedCertificateName; +}; + +const getAwsAcmClient = async ( + connectionId: string, + region: AWSRegion, + appConnectionDAL: Pick, + kmsService: Pick +): Promise => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AWS) { + throw new BadRequestError({ + message: `Connection '${connectionId}' is not an AWS connection (found: ${appConnection.app})` + }); + } + + const decryptedCredentials = await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials, + projectId: appConnection.projectId + }); + + let awsConnectionConfig: TAwsConnectionConfig; + switch (appConnection.method) { + case AwsConnectionMethod.AssumeRole: + awsConnectionConfig = { + app: AppConnection.AWS, + method: AwsConnectionMethod.AssumeRole, + credentials: decryptedCredentials as TAwsAssumeRoleCredentials, + orgId: appConnection.orgId + }; + break; + case AwsConnectionMethod.AccessKey: + awsConnectionConfig = { + app: AppConnection.AWS, + method: AwsConnectionMethod.AccessKey, + credentials: decryptedCredentials as TAwsAccessKeyCredentials, + orgId: appConnection.orgId + }; + break; + default: + throw new BadRequestError({ + message: `Unsupported AWS connection method: ${appConnection.method}` + }); + } + + const awsConfig = await getAwsConnectionConfig(awsConnectionConfig, region); + + return new AWS.ACM(awsConfig); +}; + +export const awsCertificateManagerPkiSyncFactory = ({ + kmsService, + appConnectionDAL +}: TAwsCertificateManagerPkiSyncFactoryDeps) => { + const deleteCertificateFromAcm = async ( + acm: AWS.ACM, + certificateArn: string, + operation: string, + syncId: string, + throwOnError = false + ): Promise<{ arn: string; success: boolean; error?: Error }> => { + try { + await withRateLimitRetry(() => acm.deleteCertificate({ CertificateArn: certificateArn }).promise(), { + operation, + syncId + }); + return { arn: certificateArn, success: true }; + } catch (error) { + const errorObj = error instanceof Error ? error : new Error("Unknown error"); + + if (throwOnError) { + throw new PkiSyncError({ + message: `Failed to remove certificate from AWS Certificate Manager: ${errorObj.message}`, + cause: errorObj, + context: { + certificateArn, + operation + } + }); + } + + return { + arn: certificateArn, + success: false, + error: errorObj + }; + } + }; + const $getAwsAcmCertificates = async ( + acm: AWS.ACM, + syncId = "unknown" + ): Promise<{ + acmCertificates: Record< + string, + { cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList } + >; + }> => { + const paginateAwsAcmCertificates = async () => { + const certificates: AWS.ACM.CertificateSummary[] = []; + let nextToken: string | undefined; + + do { + const listParams: AWS.ACM.ListCertificatesRequest = { + CertificateStatuses: ["ISSUED"], + NextToken: nextToken, + MaxItems: 100 + }; + + const response = await withRateLimitRetry(() => acm.listCertificates(listParams).promise(), { + operation: "list-certificates", + syncId + }); + + if (response.CertificateSummaryList) { + certificates.push(...response.CertificateSummaryList); + } + nextToken = response.NextToken; + } while (nextToken); + + return certificates; + }; + + const certificateSummaries = await paginateAwsAcmCertificates(); + + const certificateResults = await executeWithConcurrencyLimit( + certificateSummaries, + async (certSummary) => { + if (!certSummary.CertificateArn) { + throw new Error("Certificate ARN is missing"); + } + + const [certificateDetails, tagsResponse] = await Promise.all([ + acm.describeCertificate({ CertificateArn: certSummary.CertificateArn }).promise(), + acm.listTagsForCertificate({ CertificateArn: certSummary.CertificateArn }).promise() + ]); + + let certificateContent: AWS.ACM.GetCertificateResponse | undefined; + if (!shouldSkipCertificateExport(certSummary)) { + try { + certificateContent = await acm.getCertificate({ CertificateArn: certSummary.CertificateArn }).promise(); + } catch (error) { + // Certificate content cannot be imported + } + } + + return { + ...certificateDetails.Certificate, + Tags: tagsResponse.Tags, + key: extractCertificateNameFromArn(certSummary.CertificateArn), + cert: certificateContent?.Certificate || "", + certificateChain: certificateContent?.CertificateChain || "", + privateKey: "", // Private keys cannot be exported from ACM + arn: certSummary.CertificateArn + }; + }, + { operation: "fetch-certificate-details", syncId } + ); + + const successfulCertificates: ACMCertificateWithKey[] = []; + certificateResults.forEach((result) => { + if (result.status === "fulfilled") { + successfulCertificates.push(result.value as ACMCertificateWithKey); + } + }); + + const failedFetches = certificateResults.filter((result) => result.status === "rejected"); + if (failedFetches.length > 0) { + throw new PkiSyncError({ + message: `Failed to fetch ${failedFetches.length} certificate details from AWS Certificate Manager`, + shouldRetry: true, + context: { + failedCount: failedFetches.length, + totalCount: certificateSummaries.length + } + }); + } + + const res: Record< + string, + { cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList } + > = successfulCertificates.reduce( + (obj, certificate) => ({ + ...obj, + [certificate.key]: { + cert: certificate.cert, + privateKey: certificate.privateKey, + certificateChain: certificate.certificateChain, + arn: certificate.CertificateArn, + Tags: certificate.Tags + } + }), + {} as Record< + string, + { cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList } + > + ); + + return { + acmCertificates: res + }; + }; + + const syncCertificates = async ( + pkiSync: TPkiSyncWithCredentials, + certificateMap: TCertificateMap + ): Promise => { + const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig; + const acm = await getAwsAcmClient( + pkiSync.connection.id, + destinationConfig.region as AWSRegion, + appConnectionDAL, + kmsService + ); + + const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id); + + const setCertificates: CertificateImportRequest[] = []; + + const activeCertificateNames = Object.keys(certificateMap); + + Object.entries(certificateMap).forEach(([certName, certData]) => { + const { cert, privateKey, certificateChain } = certData; + const certificateName = generateCertificateName(certName, pkiSync); + + const existingCert = Object.values(acmCertificates).find((acmCert) => + validateCertificateIdentification(certName, acmCert) + ); + + const shouldUpdateCert = !existingCert || existingCert.cert !== cert; + + try { + validateCertificateContent(cert, privateKey); + } catch (validationError) { + throw new PkiSyncError({ + message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`, + shouldRetry: false, + context: { + certificateName, + certName + } + }); + } + + if (shouldUpdateCert) { + setCertificates.push({ + key: certName, + name: certificateName, + cert, + privateKey, + certificateChain, + existingArn: existingCert?.arn + }); + } + }); + + // Identify expired/removed certificates that need to be cleaned up from ACM + const certificatesToRemove = Object.values(acmCertificates) + .filter((acmCert) => { + if (!acmCert.arn || !acmCert.Tags) { + return false; + } + + const certNameTag = findInfisicalCertificateTag(acmCert.Tags); + if (!certNameTag || !certNameTag.Value) { + return false; + } + + const isActive = activeCertificateNames.includes(certNameTag.Value); + return !isActive; + }) + .map((acmCert) => acmCert.arn!) + .filter((arn) => arn); + + const uploadResults = await executeWithConcurrencyLimit( + setCertificates, + async ({ key, name, cert, privateKey, certificateChain, existingArn }) => { + try { + const importParams: AWS.ACM.ImportCertificateRequest = { + Certificate: cert, + PrivateKey: privateKey, + Tags: [ + { + Key: INFISICAL_CERTIFICATE_TAG, + Value: key + } + ] + }; + + if (certificateChain && certificateChain.trim().length > 0) { + importParams.CertificateChain = certificateChain; + } + if (existingArn) { + importParams.CertificateArn = existingArn; + } + + const response = await withRateLimitRetry(() => acm.importCertificate(importParams).promise(), { + operation: "import-certificate", + syncId: pkiSync.id + }); + + return { key, name, success: true, response }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + throw new PkiSyncError({ + message: `Failed to import certificate ${key} to AWS Certificate Manager: ${errorMessage}`, + cause: error instanceof Error ? error : new Error(errorMessage), + context: { + certificateKey: key, + certificateName: name, + region: destinationConfig.region + } + }); + } + }, + { operation: "import-certificates", syncId: pkiSync.id } + ); + + const results = uploadResults; + const failedUploads = results.filter((result) => result.status === "rejected"); + const successfulUploads = results.filter((result) => result.status === "fulfilled"); + + let removedCertificates = 0; + let failedRemovals = 0; + let removeResults: PromiseSettledResult<{ arn: string; success: boolean; error?: Error }>[] = []; + + if (certificatesToRemove.length > 0) { + removeResults = await executeWithConcurrencyLimit( + certificatesToRemove, + async (certificateArn) => deleteCertificateFromAcm(acm, certificateArn, "delete-certificate", pkiSync.id), + { operation: "remove-certificates", syncId: pkiSync.id } + ); + + const successfulRemovals = removeResults.filter( + (result) => result.status === "fulfilled" && result.value.success + ); + removedCertificates = successfulRemovals.length; + failedRemovals = removeResults.length - removedCertificates; + } + + const details: { + failedUploads?: Array<{ name: string; error: string }>; + failedRemovals?: Array<{ name: string; error: string }>; + } = {}; + + if (failedUploads.length > 0) { + details.failedUploads = failedUploads.map((failure, index) => { + const certificateName = setCertificates[index]?.name || "unknown"; + let errorMessage = "Unknown error"; + + if (failure.status === "rejected") { + errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error"; + } + + return { + name: certificateName, + error: errorMessage + }; + }); + } + + if (failedRemovals > 0 && removeResults.length > 0) { + const actualFailedRemovals = removeResults + .map((result, index) => { + if (result.status === "rejected") { + const arn = certificatesToRemove[index] || "unknown"; + const errorMessage = result.reason instanceof Error ? result.reason.message : "Unknown error"; + return { + name: arn.includes("certificate/") ? extractCertificateNameFromArn(arn) : arn, + error: errorMessage + }; + } + return null; + }) + .filter((item): item is { name: string; error: string } => item !== null); + + details.failedRemovals = actualFailedRemovals; + } + + return { + uploaded: successfulUploads.length, + removed: removedCertificates, + failedRemovals, + skipped: Object.keys(certificateMap).length - setCertificates.length, + details: Object.keys(details).length > 0 ? details : undefined + }; + }; + + const removeCertificates = async ( + pkiSync: TPkiSyncWithCredentials, + certificateNames: string[] + ): Promise => { + const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig; + const acm = await getAwsAcmClient( + pkiSync.connection.id, + destinationConfig.region as AWSRegion, + appConnectionDAL, + kmsService + ); + + const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id); + + const certificateArnsToRemove: string[] = []; + + for (const certName of certificateNames) { + const matchingCerts = Object.values(acmCertificates).filter((acmCert) => + validateCertificateIdentification(certName, acmCert) + ); + + for (const acmCert of matchingCerts) { + if (acmCert.arn) { + certificateArnsToRemove.push(acmCert.arn); + } + } + } + + const results = await executeWithConcurrencyLimit( + certificateArnsToRemove, + async (certificateArn) => + deleteCertificateFromAcm(acm, certificateArn, "delete-specific-certificate", pkiSync.id, true), + { operation: "remove-specific-certificates", syncId: pkiSync.id } + ); + + const failedRemovals = results.filter((result) => result.status === "rejected"); + + if (failedRemovals.length > 0) { + const failedReasons = failedRemovals.map((failure) => { + if (failure.status === "rejected") { + return failure.reason instanceof Error ? failure.reason.message : "Unknown error"; + } + return "Unknown error"; + }); + + throw new PkiSyncError({ + message: `Failed to remove ${failedRemovals.length} certificate(s) from AWS Certificate Manager`, + context: { + failedReasons, + totalCertificates: certificateArnsToRemove.length, + failedCount: failedRemovals.length + } + }); + } + + return { + removed: certificateArnsToRemove.length - failedRemovals.length, + failed: failedRemovals.length, + skipped: certificateNames.length - certificateArnsToRemove.length + }; + }; + + return { + syncCertificates, + removeCertificates + }; +}; diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-schemas.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-schemas.ts new file mode 100644 index 0000000000..eb9ae54444 --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-schemas.ts @@ -0,0 +1,84 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; +import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas"; + +import { AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING } from "./aws-certificate-manager-pki-sync-constants"; + +export const AwsCertificateManagerPkiSyncConfigSchema = z.object({ + region: z.nativeEnum(AWSRegion) +}); + +const AwsCertificateManagerPkiSyncOptionsSchema = z.object({ + canImportCertificates: z.boolean().default(false), + canRemoveCertificates: z.boolean().default(true), + certificateNameSchema: z + .string() + .optional() + .refine( + (schema) => { + if (!schema) return true; + + // Validate that {{certificateId}} placeholder is present + if (!schema.includes("{{certificateId}}")) { + return false; + } + + const testName = schema + .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id") + .replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env"); + + const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some( + (char) => testName.includes(char) + ); + + return ( + AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) && + !hasForbiddenChars && + testName.length >= AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MIN_LENGTH && + testName.length <= AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.MAX_LENGTH + ); + }, + { + message: + "Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager" + } + ) +}); + +export const AwsCertificateManagerPkiSyncSchema = PkiSyncSchema.extend({ + destination: z.literal(PkiSync.AwsCertificateManager), + destinationConfig: AwsCertificateManagerPkiSyncConfigSchema, + syncOptions: AwsCertificateManagerPkiSyncOptionsSchema +}); + +export const CreateAwsCertificateManagerPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().default(true), + destinationConfig: AwsCertificateManagerPkiSyncConfigSchema, + syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}), + subscriberId: z.string().optional(), + connectionId: z.string(), + projectId: z.string().trim().min(1) +}); + +export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64).optional(), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().optional(), + destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(), + syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(), + subscriberId: z.string().optional(), + connectionId: z.string().optional() +}); + +export const AwsCertificateManagerPkiSyncListItemSchema = z.object({ + name: z.literal("AWS Certificate Manager"), + connection: z.literal(AppConnection.AWS), + destination: z.literal(PkiSync.AwsCertificateManager), + canImportCertificates: z.literal(false), + canRemoveCertificates: z.literal(true) +}); diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts new file mode 100644 index 0000000000..717e86438b --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-types.ts @@ -0,0 +1,58 @@ +import * as AWS from "aws-sdk"; +import { z } from "zod"; + +import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; + +import { + AwsCertificateManagerPkiSyncConfigSchema, + AwsCertificateManagerPkiSyncSchema, + CreateAwsCertificateManagerPkiSyncSchema, + UpdateAwsCertificateManagerPkiSyncSchema +} from "./aws-certificate-manager-pki-sync-schemas"; + +export type TAwsCertificateManagerPkiSyncConfig = z.infer; + +export type TAwsCertificateManagerPkiSync = z.infer; + +export type TAwsCertificateManagerPkiSyncInput = z.infer; + +export type TAwsCertificateManagerPkiSyncUpdate = z.infer; + +export type TAwsCertificateManagerPkiSyncWithCredentials = TAwsCertificateManagerPkiSync & { + connection: TAwsConnection; +}; + +export interface ACMCertificateWithKey extends AWS.ACM.CertificateDetail { + Tags?: AWS.ACM.TagList; + key: string; + cert: string; + certificateChain: string; + privateKey: string; + arn?: string; +} + +export interface SyncCertificatesResult { + uploaded: number; + removed: number; + failedRemovals: number; + skipped: number; + details?: { + failedUploads?: Array<{ name: string; error: string }>; + failedRemovals?: Array<{ name: string; error: string }>; + }; +} + +export interface RemoveCertificatesResult { + removed: number; + failed: number; + skipped: number; +} + +export interface CertificateImportRequest { + key: string; + name: string; + cert: string; + privateKey: string; + certificateChain?: string; + existingArn?: string; +} diff --git a/backend/src/services/pki-sync/aws-certificate-manager/index.ts b/backend/src/services/pki-sync/aws-certificate-manager/index.ts new file mode 100644 index 0000000000..fb30b5c71a --- /dev/null +++ b/backend/src/services/pki-sync/aws-certificate-manager/index.ts @@ -0,0 +1,4 @@ +export * from "./aws-certificate-manager-pki-sync-constants"; +export * from "./aws-certificate-manager-pki-sync-fns"; +export * from "./aws-certificate-manager-pki-sync-schemas"; +export * from "./aws-certificate-manager-pki-sync-types"; diff --git a/backend/src/services/pki-sync/pki-sync-enums.ts b/backend/src/services/pki-sync/pki-sync-enums.ts index fadd709146..46a7fc9756 100644 --- a/backend/src/services/pki-sync/pki-sync-enums.ts +++ b/backend/src/services/pki-sync/pki-sync-enums.ts @@ -1,5 +1,6 @@ export enum PkiSync { - AzureKeyVault = "azure-key-vault" + AzureKeyVault = "azure-key-vault", + AwsCertificateManager = "aws-certificate-manager" } export enum PkiSyncStatus { diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index 343afa626f..75f312fff9 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -6,6 +6,8 @@ import { BadRequestError } from "@app/lib/errors"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants"; +import { awsCertificateManagerPkiSyncFactory } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-fns"; import { AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION } from "./azure-key-vault/azure-key-vault-pki-sync-constants"; import { azureKeyVaultPkiSyncFactory } from "./azure-key-vault/azure-key-vault-pki-sync-fns"; import { PkiSync } from "./pki-sync-enums"; @@ -14,7 +16,8 @@ import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types"; const ENTERPRISE_PKI_SYNCS: PkiSync[] = []; const PKI_SYNC_LIST_OPTIONS = { - [PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION + [PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION, + [PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION }; export const enterprisePkiSyncCheck = async ( @@ -144,8 +147,10 @@ export const matchesCertificateNameSchema = (name: string, environment: string, return name.startsWith(prefix) && name.endsWith(suffix); }; -const isAzureKeyVaultPkiSync = (pkiSync: TPkiSyncWithCredentials): boolean => { - return pkiSync.destination === PkiSync.AzureKeyVault; +const checkPkiSyncDestination = (pkiSync: TPkiSyncWithCredentials, destination: PkiSync): void => { + if (pkiSync.destination !== destination) { + throw new Error(`Invalid PKI sync destination: ${pkiSync.destination}`); + } }; export const PkiSyncFns = { @@ -163,6 +168,11 @@ export const PkiSyncFns = { "Azure Key Vault does not support importing certificates into Infisical (private keys cannot be extracted)" ); } + case PkiSync.AwsCertificateManager: { + throw new Error( + "AWS Certificate Manager does not support importing certificates into Infisical (private keys cannot be extracted)" + ); + } default: throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); } @@ -188,12 +198,15 @@ export const PkiSyncFns = { }> => { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { - if (!isAzureKeyVaultPkiSync(pkiSync)) { - throw new Error("Invalid Azure Key Vault PKI sync configuration"); - } + checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap); } + case PkiSync.AwsCertificateManager: { + checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); + const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies); + return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap); + } default: throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); } @@ -209,13 +222,17 @@ export const PkiSyncFns = { ): Promise => { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { - if (!isAzureKeyVaultPkiSync(pkiSync)) { - throw new Error("Invalid Azure Key Vault PKI sync configuration"); - } + checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames); break; } + case PkiSync.AwsCertificateManager: { + checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager); + const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies); + await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames); + break; + } default: throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`); } diff --git a/backend/src/services/pki-sync/pki-sync-maps.ts b/backend/src/services/pki-sync/pki-sync-maps.ts index b667416c6a..5c416b5130 100644 --- a/backend/src/services/pki-sync/pki-sync-maps.ts +++ b/backend/src/services/pki-sync/pki-sync-maps.ts @@ -3,9 +3,11 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { PkiSync } from "./pki-sync-enums"; export const PKI_SYNC_NAME_MAP: Record = { - [PkiSync.AzureKeyVault]: "Azure Key Vault" + [PkiSync.AzureKeyVault]: "Azure Key Vault", + [PkiSync.AwsCertificateManager]: "AWS Certificate Manager" }; export const PKI_SYNC_CONNECTION_MAP: Record = { - [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault + [PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [PkiSync.AwsCertificateManager]: AppConnection.AWS }; diff --git a/backend/src/services/project-bot/project-bot-dal.ts b/backend/src/services/project-bot/project-bot-dal.ts index ecb23f78b9..2f1863ad6d 100644 --- a/backend/src/services/project-bot/project-bot-dal.ts +++ b/backend/src/services/project-bot/project-bot-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TProjectBots, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TProjectBots, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; @@ -44,12 +44,13 @@ export const projectBotDALFactory = (db: TDbClient) => { const findProjectUserWorkspaceKey = async (projectId: string) => { try { const doc = await db - .replicaNode()(TableName.ProjectMembership) - .where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId) - .where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId` as "projectId", projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .where(`${TableName.Users}.isGhost` as "isGhost", false) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) - .join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.ProjectKeys, `${TableName.Membership}.actorUserId`, `${TableName.ProjectKeys}.receiverId`) + .where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId) .join( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, diff --git a/backend/src/services/project-key/project-key-dal.ts b/backend/src/services/project-key/project-key-dal.ts index bb91b9c855..07d061e954 100644 --- a/backend/src/services/project-key/project-key-dal.ts +++ b/backend/src/services/project-key/project-key-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TProjectKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TProjectKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; @@ -34,11 +34,12 @@ export const projectKeyDALFactory = (db: TDbClient) => { const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => { try { - const pubKeys = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + const pubKeys = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId` as "scopeProjectId", projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) - .select("userId", "publicKey"); + .select(db.ref("userId").withSchema(TableName.Users), "publicKey"); return pubKeys; } catch (error) { throw new DatabaseError({ error, name: "Find all workspace pub keys" }); diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index a884d25bc2..7faa11f2b8 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -1,25 +1,25 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { AccessScope, ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TProjectKeyDALFactory } from "./project-key-dal"; import { TGetLatestProjectKeyDTO, TUploadProjectKeyDTO } from "./project-key-types"; type TProjectKeyServiceFactoryDep = { permissionService: TPermissionServiceFactory; projectKeyDAL: TProjectKeyDALFactory; - projectMembershipDAL: TProjectMembershipDALFactory; + membershipUserDAL: TMembershipUserDALFactory; }; export type TProjectKeyServiceFactory = ReturnType; export const projectKeyServiceFactory = ({ projectKeyDAL, - projectMembershipDAL, + membershipUserDAL, permissionService }: TProjectKeyServiceFactoryDep) => { const uploadProjectKeys = async ({ @@ -42,9 +42,10 @@ export const projectKeyServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const receiverMembership = await projectMembershipDAL.findOne({ - userId: receiverId, - projectId + const receiverMembership = await membershipUserDAL.findOne({ + actorUserId: receiverId, + scopeProjectId: projectId, + scope: AccessScope.Project }); if (!receiverMembership) throw new BadRequestError({ diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index dd503c2a38..fe3b572267 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -1,15 +1,13 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TMemberships, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TProjectMembershipDALFactory = ReturnType; export const projectMembershipDALFactory = (db: TDbClient) => { - const projectMemberOrm = ormify(db, TableName.ProjectMembership); - // special query const findAllProjectMembers = async ( projectId: string, @@ -17,18 +15,17 @@ export const projectMembershipDALFactory = (db: TDbClient) => { ) => { try { const docs = await db - .replicaNode()(TableName.ProjectMembership) - .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) - .join(TableName.OrgMembership, (qb) => { - qb.on(`${TableName.Users}.id`, "=", `${TableName.OrgMembership}.userId`).andOn( - `${TableName.OrgMembership}.orgId`, - "=", - `${TableName.Project}.orgId` - ); + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId }) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(db(TableName.Membership).as("orgMembership"), (qb) => { + qb.on(`${TableName.Users}.id`, "=", `orgMembership.actorUserId`) + .andOn(`orgMembership.scopeOrgId`, "=", `${TableName.Project}.orgId`) + .andOn("orgMembership.scope", db.raw("?", [AccessScope.Organization])); }) - .where((qb) => { if (filter.usernames) { void qb.whereIn("username", filter.usernames); @@ -37,69 +34,46 @@ export const projectMembershipDALFactory = (db: TDbClient) => { void qb.where("username", filter.username); } if (filter.id) { - void qb.where(`${TableName.ProjectMembership}.id`, filter.id); + void qb.where(`${TableName.Membership}.id`, filter.id); } if (filter.roles && filter.roles.length > 0) { void qb.whereExists((subQuery) => { void subQuery .select("role") - .from(TableName.ProjectUserMembershipRole) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectRoles}.id`, - `${TableName.ProjectUserMembershipRole}.customRoleId` - ) - .whereRaw("??.?? = ??.??", [ - TableName.ProjectUserMembershipRole, - "projectMembershipId", - TableName.ProjectMembership, - "id" - ]) + .from(TableName.MembershipRole) + .leftJoin(TableName.Role, `${TableName.Role}.id`, `${TableName.MembershipRole}.customRoleId`) + .whereRaw("??.?? = ??.??", [TableName.MembershipRole, "membershipId", TableName.Membership, "id"]) .where((subQb) => { void subQb - .whereIn(`${TableName.ProjectUserMembershipRole}.role`, filter.roles as string[]) - .orWhereIn(`${TableName.ProjectRoles}.slug`, filter.roles as string[]); + .whereIn(`${TableName.MembershipRole}.role`, filter.roles as string[]) + .orWhereIn(`${TableName.Role}.slug`, filter.roles as string[]); }); }); } }) - .join( - TableName.UserEncryptionKey, - `${TableName.UserEncryptionKey}.userId`, - `${TableName.Users}.id` - ) - .join( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.ProjectMembership), - db.ref("createdAt").withSchema(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), - db.ref("publicKey").withSchema(TableName.UserEncryptionKey), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), - db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), - db.ref("isActive").withSchema(TableName.OrgMembership) + db.ref("isActive").withSchema("orgMembership") ) .where({ isGhost: false }) .orderBy(`${TableName.Users}.username` as "username"); @@ -111,7 +85,6 @@ export const projectMembershipDALFactory = (db: TDbClient) => { firstName, username, lastName, - publicKey, isGhost, id, userId, @@ -128,7 +101,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => { firstName, lastName, id: userId, - publicKey, + // akhilmhdh: if we do user encryption based join this would fail for scim user who haven't logged in yet + // public key is not used anymore as well + publicKey: "", isGhost, isOrgMembershipActive: isActive }, @@ -184,9 +159,11 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectGhostUser = async (projectId: string, tx?: Knex) => { try { - const ghostUser = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + const ghostUser = await (tx || db.replicaNode())(TableName.Membership) + .where({ [`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId }) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .select(selectAllTableCols(TableName.Users)) .where({ isGhost: true }) .first(); @@ -200,21 +177,24 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findMembershipsByUsername = async (projectId: string, usernames: string[]) => { try { const members = await db - .replicaNode()(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId }) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .join( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - selectAllTableCols(TableName.ProjectMembership), + selectAllTableCols(TableName.Membership), db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("username").withSchema(TableName.Users) ) .whereIn("username", usernames) .where({ isGhost: false }); + return members.map(({ userId, username, ...data }) => ({ ...data, user: { id: userId, username } @@ -227,45 +207,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectMembershipsByUserId = async (orgId: string, userId: string) => { try { const docs = await db - .replicaNode()(TableName.ProjectMembership) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.id`, userId) .where(`${TableName.Project}.orgId`, orgId) - .join( - TableName.UserEncryptionKey, - `${TableName.UserEncryptionKey}.userId`, - `${TableName.Users}.id` - ) - .join( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), - db.ref("publicKey").withSchema(TableName.UserEncryptionKey), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), - db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("id").as("projectId").withSchema(TableName.Project), db.ref("type").as("projectType").withSchema(TableName.Project) @@ -274,22 +242,21 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const members = sqlNestRelationships({ data: docs, - parentMapper: ({ - email, - firstName, - username, - lastName, - publicKey, - isGhost, - id, - projectId, - projectName, - projectType - }) => ({ + parentMapper: ({ email, firstName, username, lastName, isGhost, id, projectId, projectName, projectType }) => ({ id, userId, projectId, - user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, + user: { + email, + username, + firstName, + lastName, + id: userId, + isGhost, + // akhilmhdh: if we do user encryption based join this would fail for scim user who haven't logged in yet + // public key is not used anymore as well + publicKey: "" + }, project: { id: projectId, name: projectName, @@ -336,9 +303,11 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectMembershipsByUserIds = async (orgId: string, userIds: string[]) => { try { const docs = await db - .replicaNode()(TableName.ProjectMembership) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .whereIn(`${TableName.Users}.id`, userIds) .where(`${TableName.Project}.orgId`, orgId) .join( @@ -346,18 +315,10 @@ export const projectMembershipDALFactory = (db: TDbClient) => { `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) - .join( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), @@ -365,16 +326,16 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), - db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("id").as("projectId").withSchema(TableName.Project), db.ref("type").as("projectType").withSchema(TableName.Project) @@ -444,7 +405,6 @@ export const projectMembershipDALFactory = (db: TDbClient) => { }; return { - ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByUsername, diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 301ce1bc04..b0949d550a 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -1,64 +1,51 @@ /* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { - constructPermissionErrorMessage, - validatePrivilegeChangeOperation -} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; -import { ms } from "@app/lib/ms"; import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; import { ActorType } from "../auth/auth-type"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TNotificationServiceFactory } from "../notification/notification-service"; import { NotificationType } from "../notification/notification-types"; -import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TProjectMembershipDALFactory } from "./project-membership-dal"; import { - ProjectUserMembershipTemporaryMode, TAddUsersToWorkspaceDTO, - TDeleteProjectMembershipOldDTO, TDeleteProjectMembershipsDTO, - TGetProjectMembershipByIdDTO, TGetProjectMembershipByUsernameDTO, TGetProjectMembershipDTO, - TLeaveProjectDTO, - TUpdateProjectMembershipDTO + TLeaveProjectDTO } from "./project-membership-types"; -import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal"; type TProjectMembershipServiceFactoryDep = { permissionService: Pick< TPermissionServiceFactory, - "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" + "getProjectPermission" | "getProjectPermissionByRoles" | "invalidateProjectPermissionCache" >; smtpService: TSmtpService; - projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; - projectUserMembershipRoleDAL: Pick; - userDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: Pick; + userDAL: Pick; userGroupMembershipDAL: TUserGroupMembershipDALFactory; - projectRoleDAL: Pick; - orgDAL: Pick; projectDAL: Pick; projectKeyDAL: Pick; licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; + additionalPrivilegeDAL: Pick; secretReminderRecipientsDAL: Pick; groupProjectDAL: TGroupProjectDALFactory; notificationService: Pick; @@ -69,19 +56,17 @@ export type TProjectMembershipServiceFactory = ReturnType { const getProjectMemberships = async ({ actorId, @@ -150,29 +135,6 @@ export const projectMembershipServiceFactory = ({ return membership; }; - const getProjectMembershipById = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - projectId, - id - }: TGetProjectMembershipByIdDTO) => { - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - - const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id }); - if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` }); - return membership; - }; - const addUsersToProject = async ({ projectId, actorId, @@ -194,48 +156,58 @@ export const projectMembershipServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); - const orgMembers = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId, + const orgMembers = await membershipUserDAL.find({ + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: project.orgId, + scope: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId) + [`${TableName.Membership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId) } }); + if (orgMembers.length !== members.length) throw new BadRequestError({ message: "Some users are not part of org" }); - const existingMembers = await projectMembershipDAL.find({ - projectId, - $in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) } + const existingMembers = await membershipUserDAL.find({ + [`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId, + scope: AccessScope.Project, + $in: { actorUserId: orgMembers.map(({ actorUserId }) => actorUserId).filter(Boolean) } }); if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" }); + const orgMembershipUsernames = await userDAL.find({ + $in: { + id: orgMembers.filter((el) => Boolean(el.actorUserId)).map((el) => el.actorUserId as string) + } + }); const userIdsToExcludeForProjectKeyAddition = new Set( await userGroupMembershipDAL.findUserGroupMembershipsInProject( - orgMembers.map(({ username }) => username), + orgMembershipUsernames.map(({ username }) => username), projectId ) ); - await projectMembershipDAL.transaction(async (tx) => { - const projectMemberships = await projectMembershipDAL.insertMany( - orgMembers.map(({ userId }) => ({ - projectId, - userId + await membershipUserDAL.transaction(async (tx) => { + const projectMemberships = await membershipUserDAL.insertMany( + orgMembers.map(({ actorUserId }) => ({ + scopeProjectId: projectId, + actorUserId, + scope: AccessScope.Project, + scopeOrgId: project.orgId })), tx ); - await projectUserMembershipRoleDAL.insertMany( - projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })), + await membershipRoleDAL.insertMany( + projectMemberships.map(({ id }) => ({ membershipId: id, role: ProjectMembershipRole.Member })), tx ); const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); await projectKeyDAL.insertMany( orgMembers - .filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId)) - .map(({ userId, id }) => ({ + .filter(({ actorUserId }) => !userIdsToExcludeForProjectKeyAddition.has(actorUserId as string)) + .map(({ actorUserId, id }) => ({ encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, senderId: actorId, - receiverId: userId, + receiverId: actorUserId as string, projectId })), tx @@ -246,8 +218,8 @@ export const projectMembershipServiceFactory = ({ if (sendEmails) { await notificationService.createUserNotifications( - orgMembers.map((member) => ({ - userId: member.userId, + orgMembershipUsernames.map((member) => ({ + userId: member.id, orgId: project.orgId, type: NotificationType.PROJECT_INVITATION, title: "Project Invitation", @@ -259,7 +231,7 @@ export const projectMembershipServiceFactory = ({ await smtpService.sendMail({ template: SmtpTemplates.WorkspaceInvite, subjectLine: "Infisical project invitation", - recipients: orgMembers.filter((i) => i.email).map((i) => i.email as string), + recipients: orgMembershipUsernames.filter((i) => i.email).map((i) => i.email as string), substitutions: { workspaceName: project.name, callback_url: `${appCfg.SITE_URL}/login` @@ -269,164 +241,6 @@ export const projectMembershipServiceFactory = ({ return orgMembers; }; - const updateProjectMembership = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - projectId, - membershipId, - roles - }: TUpdateProjectMembershipDTO) => { - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - - const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); - if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) { - throw new ForbiddenRequestError({ message: "Forbidden member update" }); - } - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - projectId - ); - - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - `Failed to change role ${requestedRoleChange}`, - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => - !Object.values(ProjectMembershipRole) - // we don't want to include custom in this check; - // this unintentionally enables setting slug to custom which is reserved - .filter((r) => r !== ProjectMembershipRole.Custom) - .includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - if (hasCustomRole) { - const plan = await licenseService.getPlan(actorOrgId); - if (!plan?.rbac) - throw new BadRequestError({ - message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member." - }); - } - - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) { - throw new NotFoundError({ message: "One or more custom roles not found" }); - } - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: membershipId, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: membershipId, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const updatedRoles = await projectMembershipDAL.transaction(async (tx) => { - await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx); - return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return updatedRoles; - }; - - // This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now. - const deleteProjectMembership = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - projectId, - membershipId - }: TDeleteProjectMembershipOldDTO) => { - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); - - const member = await userDAL.findUserByProjectMembershipId(membershipId); - - if (member?.isGhost) { - throw new ForbiddenRequestError({ - message: "Forbidden membership deletion", - name: "DeleteProjectMembership" - }); - } - - const membership = await projectMembershipDAL.transaction(async (tx) => { - const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx); - await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx); - await secretReminderRecipientsDAL.delete( - { - projectId, - userId: deletedMembership.userId - }, - tx - ); - return deletedMembership; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return membership; - }; - const deleteProjectMemberships = async ({ actorId, actor, @@ -478,20 +292,21 @@ export const projectMembershipServiceFactory = ({ await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId) ); - const memberships = await projectMembershipDAL.transaction(async (tx) => { - await projectUserAdditionalPrivilegeDAL.delete( + const memberships = await membershipUserDAL.transaction(async (tx) => { + await additionalPrivilegeDAL.delete( { projectId, $in: { - userId: projectMembers.map((membership) => membership.user.id) + actorUserId: projectMembers.map((membership) => membership.user.id) } }, tx ); - const deletedMemberships = await projectMembershipDAL.delete( + const deletedMemberships = await membershipUserDAL.delete( { - projectId, + scopeProjectId: projectId, + scope: AccessScope.Project, $in: { id: projectMembers.map(({ id }) => id) } @@ -564,11 +379,11 @@ export const projectMembershipServiceFactory = ({ }); } - const deletedMembership = await projectMembershipDAL.transaction(async (tx) => { - await projectUserAdditionalPrivilegeDAL.delete( + const deletedMembership = await membershipUserDAL.transaction(async (tx) => { + await additionalPrivilegeDAL.delete( { projectId: project.id, - userId: actorId + actorUserId: actorId }, tx ); @@ -582,10 +397,11 @@ export const projectMembershipServiceFactory = ({ ); const membership = ( - await projectMembershipDAL.delete( + await membershipUserDAL.delete( { - projectId: project.id, - userId: actorId + scope: AccessScope.Project, + scopeProjectId: project.id, + actorUserId: actorId }, tx ) @@ -603,11 +419,8 @@ export const projectMembershipServiceFactory = ({ return { getProjectMemberships, getProjectMembershipByUsername, - updateProjectMembership, deleteProjectMemberships, - deleteProjectMembership, // TODO: Remove this addUsersToProject, - leaveProject, - getProjectMembershipById + leaveProject }; }; diff --git a/backend/src/services/project-membership/project-user-membership-role-dal.ts b/backend/src/services/project-membership/project-user-membership-role-dal.ts deleted file mode 100644 index b1cb55b9be..0000000000 --- a/backend/src/services/project-membership/project-user-membership-role-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TProjectUserMembershipRoleDALFactory = ReturnType; - -export const projectUserMembershipRoleDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.ProjectUserMembershipRole); - return orm; -}; diff --git a/backend/src/services/project-role/project-role-dal.ts b/backend/src/services/project-role/project-role-dal.ts deleted file mode 100644 index 942fefa115..0000000000 --- a/backend/src/services/project-role/project-role-dal.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TProjectRoleDALFactory = ReturnType; - -export const projectRoleDALFactory = (db: TDbClient) => ormify(db, TableName.ProjectRoles); diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts deleted file mode 100644 index f30da21f53..0000000000 --- a/backend/src/services/project-role/project-role-service.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; -import { requestContext } from "@fastify/request-context"; - -import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { - ProjectPermissionActions, - ProjectPermissionSet, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; - -import { ActorAuthMethod, ActorType } from "../auth/auth-type"; -import { TIdentityDALFactory } from "../identity/identity-dal"; -import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; -import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; -import { TUserDALFactory } from "../user/user-dal"; -import { TProjectRoleDALFactory } from "./project-role-dal"; -import { getPredefinedRoles } from "./project-role-fns"; -import { - ProjectRoleServiceIdentifierType, - TCreateRoleDTO, - TDeleteRoleDTO, - TGetRoleDetailsDTO, - TListRolesDTO, - TUpdateRoleDTO -} from "./project-role-types"; - -type TProjectRoleServiceFactoryDep = { - projectRoleDAL: TProjectRoleDALFactory; - identityDAL: Pick; - userDAL: Pick; - projectDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getUserProjectPermission" | "invalidateProjectPermissionCache" - >; - identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; - projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; -}; - -export type TProjectRoleServiceFactory = ReturnType; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -export const projectRoleServiceFactory = ({ - projectRoleDAL, - permissionService, - identityProjectMembershipRoleDAL, - projectUserMembershipRoleDAL, - projectDAL, - identityDAL, - userDAL -}: TProjectRoleServiceFactoryDep) => { - const createRole = async ({ data, actor, actorId, actorAuthMethod, actorOrgId, filter }: TCreateRoleDTO) => { - let projectId = ""; - if (filter.type === ProjectRoleServiceIdentifierType.SLUG) { - const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: "Project not found" }); - projectId = project.id; - } else { - projectId = filter.projectId; - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role); - const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); - if (existingRole) { - throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" }); - } - - validateHandlebarTemplate("Project Role Create", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - const role = await projectRoleDAL.create({ - ...data, - projectId - }); - return { ...role, permissions: unpackPermissions(role.permissions) }; - }; - - const getRoleBySlug = async ({ - actor, - actorId, - actorAuthMethod, - actorOrgId, - roleSlug, - filter - }: TGetRoleDetailsDTO) => { - let project: TProjects; - if (filter.type === ProjectRoleServiceIdentifierType.SLUG) { - project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); - } else { - project = await projectDAL.findProjectById(filter.projectId); - } - - if (!project) throw new NotFoundError({ message: "Project not found" }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: project.id, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); - if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) { - const [predefinedRole] = getPredefinedRoles({ - projectId: project.id, - projectType: project.type as ProjectType, - roleFilter: roleSlug as ProjectMembershipRole - }); - - if (!predefinedRole) throw new NotFoundError({ message: `Default role with slug '${roleSlug}' not found` }); - - return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) }; - } - - const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId: project.id }); - if (!customRole) throw new NotFoundError({ message: `Project role with slug '${roleSlug}' not found` }); - return { ...customRole, permissions: unpackPermissions(customRole.permissions) }; - }; - - const updateRole = async ({ roleId, actorOrgId, actorAuthMethod, actorId, actor, data }: TUpdateRoleDTO) => { - const projectRole = await projectRoleDAL.findById(roleId); - if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectRole.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); - - if (data?.slug) { - const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId: projectRole.projectId }); - if (existingRole && existingRole.id !== roleId) - throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" }); - } - validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const updatedRole = await projectRoleDAL.updateById(projectRole.id, { - ...data, - permissions: data.permissions ? data.permissions : undefined - }); - if (!updatedRole) throw new NotFoundError({ message: "Project role not found", name: "Update role" }); - - await permissionService.invalidateProjectPermissionCache(projectRole.projectId); - - return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) }; - }; - - const deleteRole = async ({ actor, actorId, actorAuthMethod, actorOrgId, roleId }: TDeleteRoleDTO) => { - const projectRole = await projectRoleDAL.findById(roleId); - if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectRole.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); - - const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId }); - const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId }); - - if (identityRole) { - throw new BadRequestError({ - message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.", - name: "Delete role" - }); - } - if (projectUserRole) { - throw new BadRequestError({ - message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.", - name: "Delete role" - }); - } - - const deletedRole = await projectRoleDAL.deleteById(roleId); - if (!deletedRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" }); - - await permissionService.invalidateProjectPermissionCache(projectRole.projectId); - - return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) }; - }; - - const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => { - let project: TProjects; - if (filter.type === ProjectRoleServiceIdentifierType.SLUG) { - project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); - } else { - project = await projectDAL.findProjectById(filter.projectId); - } - - if (!project) throw new BadRequestError({ message: "Project not found" }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: project.id, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); - const customRoles = await projectRoleDAL.find( - { projectId: project.id }, - { sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] } - ); - const roles = [ - ...getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType }), - ...(customRoles || []) - ]; - - return roles; - }; - - const getUserPermission = async ( - userId: string, - projectId: string, - actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => { - const { permission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: userId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - // just to satisfy ts - if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" }); - - const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails"); - const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === projectId; - const assumedPrivilegeDetails = isAssumingPrivilege - ? { - actorId: assumedPrivilegeDetailsCtx?.actorId, - actorType: assumedPrivilegeDetailsCtx?.actorType, - actorName: "", - actorEmail: "" - } - : undefined; - - if (assumedPrivilegeDetails?.actorType === ActorType.IDENTITY) { - const identityDetails = await identityDAL.findById(assumedPrivilegeDetails.actorId); - if (!identityDetails) - throw new NotFoundError({ message: `Identity with ID ${assumedPrivilegeDetails.actorId} not found` }); - assumedPrivilegeDetails.actorName = identityDetails.name; - } else if (assumedPrivilegeDetails?.actorType === ActorType.USER) { - const userDetails = await userDAL.findById(assumedPrivilegeDetails?.actorId); - if (!userDetails) - throw new NotFoundError({ message: `User with ID ${assumedPrivilegeDetails.actorId} not found` }); - assumedPrivilegeDetails.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`; - assumedPrivilegeDetails.actorEmail = userDetails?.email || ""; - } - - return { permissions: packRules(permission.rules), membership, assumedPrivilegeDetails }; - }; - - return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug }; -}; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index d64977f8bf..2abdebdbcb 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, ProjectsSchema, ProjectType, ProjectUpgradeStatus, @@ -24,9 +25,11 @@ export const projectDALFactory = (db: TDbClient) => { const findIdentityProjects = async (identityId: string, orgId: string, projectType?: ProjectType) => { try { - const workspaces = await db(TableName.IdentityProjectMembership) - .where({ identityId }) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + const workspaces = await db + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.actorIdentityId`, identityId) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.orgId`, orgId) .andWhere((qb) => { if (projectType) { @@ -74,11 +77,23 @@ export const projectDALFactory = (db: TDbClient) => { const findUserProjects = async (userId: string, orgId: string, projectType?: ProjectType) => { try { - const workspaces = await db - .replicaNode()(TableName.ProjectMembership) - .where({ userId }) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + const userGroupSubquery = db + .replicaNode()(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, orgId) + .where(`${TableName.UserGroupMembership}.userId`, userId) + .select(db.ref("id").withSchema(TableName.Groups)); + + const projects = await db + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + void qb + .where(`${TableName.Membership}.actorUserId`, userId) + .orWhereIn(`${TableName.Membership}.actorGroupId`, userGroupSubquery); + }) .andWhere((qb) => { if (projectType) { void qb.where(`${TableName.Project}.type`, projectType); @@ -97,36 +112,8 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Environment}.position`, order: "asc" } ]); - const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId"); - - const groupWorkspaces = await db(TableName.GroupProjectMembership) - .whereIn("groupId", groups) - .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) - .where(`${TableName.Project}.orgId`, orgId) - .andWhere((qb) => { - if (projectType) { - void qb.where(`${TableName.Project}.type`, projectType); - } - }) - .whereNotIn( - `${TableName.Project}.id`, - workspaces.map(({ id }) => id) - ) - .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) - .select( - selectAllTableCols(TableName.Project), - db.ref("id").withSchema(TableName.Project).as("_id"), - db.ref("id").withSchema(TableName.Environment).as("envId"), - db.ref("slug").withSchema(TableName.Environment).as("envSlug"), - db.ref("name").withSchema(TableName.Environment).as("envName") - ) - .orderBy([ - { column: `${TableName.Project}.name`, order: "asc" }, - { column: `${TableName.Environment}.position`, order: "asc" } - ]); - - const nestedWorkspaces = sqlNestRelationships({ - data: workspaces.concat(groupWorkspaces), + const formattedProjects = sqlNestRelationships({ + data: projects, key: "id", parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), childrenMapper: [ @@ -142,7 +129,7 @@ export const projectDALFactory = (db: TDbClient) => { ] }); - return nestedWorkspaces.map((workspace) => ({ + return formattedProjects.map((workspace) => ({ ...workspace, organization: workspace.orgId })); @@ -153,9 +140,10 @@ export const projectDALFactory = (db: TDbClient) => { const findProjectGhostUser = async (projectId: string, tx?: Knex) => { try { - const ghostUser = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + const ghostUser = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .select(selectAllTableCols(TableName.Users)) .where({ isGhost: true }) .first(); @@ -177,54 +165,6 @@ export const projectDALFactory = (db: TDbClient) => { } }; - const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => { - try { - const workspaces = await db - .replicaNode()(TableName.IdentityProjectMembership) - .where({ identityId }) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) - .andWhere((qb) => { - if (projectType) { - void qb.where(`${TableName.Project}.type`, projectType); - } - }) - .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) - .select( - selectAllTableCols(TableName.Project), - db.ref("id").withSchema(TableName.Project).as("_id"), - db.ref("id").withSchema(TableName.Environment).as("envId"), - db.ref("slug").withSchema(TableName.Environment).as("envSlug"), - db.ref("name").withSchema(TableName.Environment).as("envName") - ) - .orderBy("createdAt", "asc", "last"); - - const nestedWorkspaces = sqlNestRelationships({ - data: workspaces, - key: "id", - parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), - childrenMapper: [ - { - key: "envId", - label: "environments" as const, - mapper: ({ envId: id, envSlug: slug, envName: name }) => ({ - id, - slug, - name - }) - } - ] - }); - - // We need to add the organization field, as it's required for one of our API endpoint responses. - return nestedWorkspaces.map((workspace) => ({ - ...workspace, - organization: workspace.orgId - })); - } catch (error) { - throw new DatabaseError({ error, name: "Find all projects by identity" }); - } - }; - const findProjectById = async (id: string) => { try { const workspaces = await db @@ -402,19 +342,26 @@ export const projectDALFactory = (db: TDbClient) => { projectIds?: string[]; }) => { const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto; - - const userMembershipSubquery = db(TableName.ProjectMembership).where({ userId: dto.actorId }).select("projectId"); - const groups = db(TableName.UserGroupMembership).where({ userId: dto.actorId }).select("groupId"); - const groupMembershipSubquery = db(TableName.GroupProjectMembership).whereIn("groupId", groups).select("projectId"); - - const identityMembershipSubQuery = db(TableName.IdentityProjectMembership) - .where({ identityId: dto.actorId }) - .select("projectId"); + const groupMembershipSubquery = db(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, dto.orgId) + .where(`${TableName.UserGroupMembership}.userId`, dto.actorId) + .select(db.ref("id").withSchema(TableName.Groups)); + const membershipSubQuery = db(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where((qb) => { + if (dto.actor === ActorType.IDENTITY) { + void qb.where(`${TableName.Membership}.actorIdentityId`, dto.actorId); + } else { + void qb + .where(`${TableName.Membership}.actorUserId`, dto.actorId) + .orWhereIn(`${TableName.Membership}.actorGroupId`, groupMembershipSubquery); + } + }) + .select("scopeProjectId"); // Get the SQL strings for the subqueries - const userMembershipSql = userMembershipSubquery.toQuery(); - const groupMembershipSql = groupMembershipSubquery.toQuery(); - const identityMembershipSql = identityMembershipSubQuery.toQuery(); + const membershipSQL = membershipSubQuery.toQuery(); const query = db .replicaNode()(TableName.Project) @@ -422,26 +369,15 @@ export const projectDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.Project)) .select(db.raw("COUNT(*) OVER() AS count")) .select<(TProjects & { isMember: boolean; count: number })[]>( - dto.actor === ActorType.USER - ? db.raw( - ` - CASE - WHEN ${TableName.Project}.id IN (?) THEN TRUE - WHEN ${TableName.Project}.id IN (?) THEN TRUE - ELSE FALSE - END as "isMember" - `, - [db.raw(userMembershipSql), db.raw(groupMembershipSql)] - ) - : db.raw( - ` - CASE - WHEN ${TableName.Project}.id IN (?) THEN TRUE - ELSE FALSE - END as "isMember" - `, - [db.raw(identityMembershipSql)] - ) + db.raw( + ` + CASE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + ELSE FALSE + END as "isMember" + `, + [db.raw(membershipSQL)] + ) ) .limit(limit) .offset(offset); @@ -495,7 +431,6 @@ export const projectDALFactory = (db: TDbClient) => { findUserProjects, findIdentityProjects, setProjectUpgradeStatus, - findAllProjectsByIdentity, findProjectGhostUser, findProjectById, findProjectByFilter, diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index e557b7c96c..30ec6d29fb 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -1,5 +1,6 @@ /* eslint-disable no-await-in-loop */ import { + AccessScope, IntegrationAuthsSchema, ProjectMembershipRole, ProjectUpgradeStatus, @@ -29,13 +30,13 @@ import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -55,13 +56,13 @@ type TProjectQueueFactoryDep = { secretApprovalSecretDAL: Pick; projectBotDAL: Pick; orgService: Pick; - projectMembershipDAL: Pick; - projectUserMembershipRoleDAL: Pick; integrationAuthDAL: TIntegrationAuthDALFactory; userDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; orgDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; }; export const projectQueueFactory = ({ @@ -79,8 +80,8 @@ export const projectQueueFactory = ({ orgDAL, projectDAL, orgService, - projectMembershipDAL, - projectUserMembershipRoleDAL + membershipUserDAL, + membershipRoleDAL }: TProjectQueueFactoryDep) => { const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => { await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, { @@ -227,17 +228,16 @@ export const projectQueueFactory = ({ ); // Create a membership for the ghost user - const projectMembership = await projectMembershipDAL.create( + const projectMembership = await membershipUserDAL.create( { - projectId: project.id, - userId: ghostUser.user.id + scopeProjectId: project.id, + scope: AccessScope.Project, + actorUserId: ghostUser.user.id, + scopeOrgId: project.orgId }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); + await membershipRoleDAL.create({ membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, tx); // If a bot already exists, delete it if (existingBot) { @@ -272,8 +272,9 @@ export const projectQueueFactory = ({ for (const key of existingProjectKeys) { const user = await userDAL.findUserEncKeyByUserId(key.receiverId); const [orgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: key.receiverId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: key.receiverId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: project.orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!user) { diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 8dbba2b4f1..58b3c5395e 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -3,6 +3,7 @@ import { PackRule, unpackRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import { + AccessScope, ActionProjectType, ProjectMembershipRole, ProjectType, @@ -49,11 +50,11 @@ import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { expandInternalCa } from "../certificate-authority/certificate-authority-fns"; import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; -import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; -import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; -import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "../membership-group/membership-group-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns"; import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; @@ -65,10 +66,9 @@ import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { getPredefinedRoles } from "../project-role/project-role-fns"; import { TReminderServiceFactory } from "../reminder/reminder-types"; +import { TRoleDALFactory } from "../role/role-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -125,7 +125,6 @@ export const DEFAULT_PROJECT_ENVS = [ type TProjectServiceFactoryDep = { projectDAL: TProjectDALFactory; - identityProjectDAL: Pick; projectSshConfigDAL: Pick; projectQueue: TProjectQueueFactory; userDAL: TUserDALFactory; @@ -134,13 +133,11 @@ type TProjectServiceFactoryDep = { secretDAL: Pick; secretV2BridgeDAL: Pick; projectEnvDAL: Pick; - identityOrgMembershipDAL: TIdentityOrgDALFactory; - identityProjectMembershipRoleDAL: Pick; - projectMembershipDAL: Pick< - TProjectMembershipDALFactory, - "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" - >; - groupProjectDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: Pick; + membershipGroupDAL: Pick; + membershipIdentityDAL: Pick; + membershipRoleDAL: Pick; projectSlackConfigDAL: Pick< TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create" | "delete" @@ -154,7 +151,6 @@ type TProjectServiceFactoryDep = { TMicrosoftTeamsIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails" >; - projectUserMembershipRoleDAL: Pick; pkiSubscriberDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; @@ -172,7 +168,7 @@ type TProjectServiceFactoryDep = { smtpService: Pick; orgDAL: Pick; keyStore: Pick; - projectRoleDAL: Pick; + roleDAL: Pick; kmsService: Pick< TKmsServiceFactory, | "updateProjectSecretManagerKmsKey" @@ -201,20 +197,15 @@ export const projectServiceFactory = ({ orgDAL, userDAL, folderDAL, - identityOrgMembershipDAL, projectMembershipDAL, projectEnvDAL, licenseService, - projectUserMembershipRoleDAL, - projectRoleDAL, certificateAuthorityDAL, certificateDAL, certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, pkiSubscriberDAL, - identityProjectDAL, - identityProjectMembershipRoleDAL, sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateDAL, @@ -228,10 +219,13 @@ export const projectServiceFactory = ({ slackIntegrationDAL, microsoftTeamsIntegrationDAL, projectTemplateService, - groupProjectDAL, smtpService, reminderService, - notificationService + notificationService, + membershipIdentityDAL, + membershipUserDAL, + membershipRoleDAL, + roleDAL }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -345,7 +339,7 @@ export const projectServiceFactory = ({ tx ); } - await projectRoleDAL.insertMany( + await roleDAL.insertMany( projectTemplate.packedRoles.map((role) => ({ ...role, permissions: JSON.stringify(role.permissions), @@ -374,15 +368,17 @@ export const projectServiceFactory = ({ } // Create a membership for the user - const userProjectMembership = await projectMembershipDAL.create( + const userProjectMembership = await membershipUserDAL.create( { - projectId: project.id, - userId: user.id + scopeProjectId: project.id, + actorUserId: user.id, + scope: AccessScope.Project, + scopeOrgId: project.orgId }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, + await membershipRoleDAL.create( + { membershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, tx ); } @@ -390,10 +386,11 @@ export const projectServiceFactory = ({ // If the project is being created by an identity, add the identity to the project as an admin else if (actor === ActorType.IDENTITY) { // Find identity org membership - const identityOrgMembership = await identityOrgMembershipDAL.findOne( + const identityOrgMembership = await membershipIdentityDAL.findOne( { - identityId: actorId, - orgId: project.orgId + actorIdentityId: actorId, + scopeOrgId: project.orgId, + scope: AccessScope.Organization }, tx ); @@ -405,17 +402,19 @@ export const projectServiceFactory = ({ }); } - const identityProjectMembership = await identityProjectDAL.create( + const identityProjectMembership = await membershipIdentityDAL.create( { - identityId: actorId, - projectId: project.id + actorIdentityId: actorId, + scopeProjectId: project.id, + scope: AccessScope.Project, + scopeOrgId: project.orgId }, tx ); - await identityProjectMembershipRoleDAL.create( + await membershipRoleDAL.create( { - projectMembershipId: identityProjectMembership.id, + membershipId: identityProjectMembership.id, role: ProjectMembershipRole.Admin }, tx @@ -459,8 +458,11 @@ export const projectServiceFactory = ({ const deletedProject = await projectDAL.transaction(async (tx) => { // delete these so that project custom roles can be deleted in cascade effect // direct deletion of project without these will cause fk error - await projectMembershipDAL.delete({ projectId: project.id }, tx); - await groupProjectDAL.delete({ projectId: project.id }, tx); + // this will clean up all memberships + await membershipUserDAL.delete( + { scopeOrgId: project.orgId, scopeProjectId: project.id, scope: AccessScope.Project }, + tx + ); const delProject = await projectDAL.deleteById(project.id, tx); const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null); // akhilmhdh: before removing those kms checking any other project uses it @@ -511,7 +513,8 @@ export const projectServiceFactory = ({ : await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { - const { permission } = await permissionService.getUserOrgPermission( + const { permission } = await permissionService.getOrgPermission( + actor, actorId, actorOrgId, actorAuthMethod, @@ -520,13 +523,13 @@ export const projectServiceFactory = ({ // `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); - const customRoles = await projectRoleDAL.find({ + const customRoles = await roleDAL.find({ $in: { projectId: workspaces.map((workspace) => workspace.id) } }); - const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId); + const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId as string); const workspacesWithRoles = await Promise.all( workspaces.map(async (workspace) => { @@ -1346,7 +1349,7 @@ export const projectServiceFactory = ({ }; const getProjectKmsKeys = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TGetProjectKmsKey) => { - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -1355,10 +1358,6 @@ export const projectServiceFactory = ({ actionProjectType: ActionProjectType.Any }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } - const kmsKeyId = await kmsService.getProjectSecretManagerKmsKeyId(projectId); const kmsKey = await kmsService.getKmsById(kmsKeyId); @@ -1875,7 +1874,7 @@ export const projectServiceFactory = ({ .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) .map((el) => el.user.email!); if (filteredProjectMembers.length === 0) { - const customRolesWithMemberCreate = await projectRoleDAL.find({ projectId }); + const customRolesWithMemberCreate = await roleDAL.find({ projectId }); const customRoleSlugsCanCreate = customRolesWithMemberCreate .filter((role) => { try { diff --git a/backend/src/services/role/namespace/namespace-role-factory.ts b/backend/src/services/role/namespace/namespace-role-factory.ts new file mode 100644 index 0000000000..87921c25b6 --- /dev/null +++ b/backend/src/services/role/namespace/namespace-role-factory.ts @@ -0,0 +1,47 @@ +import { AccessScope } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError } from "@app/lib/errors"; + +import { TRoleScopeFactory } from "../role-types"; + +type TNamespaceRoleScopeFactoryDep = { + permissionService: Pick; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const newNamespaceRoleFactory = (_dto: TNamespaceRoleScopeFactoryDep): TRoleScopeFactory => { + const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async () => {}; + + const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async () => {}; + + const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async () => {}; + + const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async () => {}; + + const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async () => {}; + + const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async () => {}; + + const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const isCustomRole: TRoleScopeFactory["isCustomRole"] = () => false; + + const getPredefinedRoles: TRoleScopeFactory["getPredefinedRoles"] = async () => []; + + return { + onCreateRoleGuard, + onUpdateRoleGuard, + onDeleteRoleGuard, + onListRoleGuard, + onGetRoleByIdGuard, + onGetRoleBySlugGuard, + getScopeField, + isCustomRole, + getPredefinedRoles + }; +}; diff --git a/backend/src/services/role/org/org-role-factory.ts b/backend/src/services/role/org/org-role-factory.ts new file mode 100644 index 0000000000..50ffa5e43a --- /dev/null +++ b/backend/src/services/role/org/org-role-factory.ts @@ -0,0 +1,160 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope } from "@app/db/schemas"; +import { + orgAdminPermissions, + orgMemberPermissions, + orgNoAccessPermissions, + OrgPermissionActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError } from "@app/lib/errors"; +import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; + +import { TRoleScopeFactory } from "../role-types"; + +type TOrgRoleScopeFactoryDep = { + permissionService: Pick; + externalGroupOrgRoleMappingDAL: Pick; +}; + +export const newOrgRoleFactory = ({ + permissionService, + externalGroupOrgRoleMappingDAL +}: TOrgRoleScopeFactoryDep): TRoleScopeFactory => { + const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const isCustomRole: TRoleScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role); + }; + + const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); + }; + + const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); + + const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ + orgId: dto.permission.orgId, + roleId: dto.selector.id + }); + + if (externalGroupMapping) + throw new BadRequestError({ + message: + "Cannot delete role assigned to external group organization role mapping. Please re-assign external mapping and try again." + }); + }; + + const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + }; + + const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + }; + + const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + }; + + const getPredefinedRoles: TRoleScopeFactory["getPredefinedRoles"] = async (scopeData) => { + const scopeField = getScopeField(scopeData); + return [ + { + id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid + name: "Admin", + slug: "admin", + orgId: scopeField.value, + description: "Complete administration access over the organization", + permissions: orgAdminPermissions, + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response + name: "Member", + slug: "member", + orgId: scopeField.value, + description: "Non-administrative role in an organization", + permissions: orgMemberPermissions, + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response + name: "No Access", + slug: "no-access", + orgId: scopeField.value, + description: "No access to any resources in the organization", + permissions: orgNoAccessPermissions, + createdAt: new Date(), + updatedAt: new Date() + } + ]; + }; + + return { + onCreateRoleGuard, + onUpdateRoleGuard, + onDeleteRoleGuard, + onListRoleGuard, + onGetRoleByIdGuard, + onGetRoleBySlugGuard, + getScopeField, + getPredefinedRoles, + isCustomRole + }; +}; diff --git a/backend/src/services/role/project/project-role-factory.ts b/backend/src/services/role/project/project-role-factory.ts new file mode 100644 index 0000000000..2ab4e59d03 --- /dev/null +++ b/backend/src/services/role/project/project-role-factory.ts @@ -0,0 +1,203 @@ +import { ForbiddenError } from "@casl/ability"; +import { v4 as uuidv4 } from "uuid"; + +import { AccessScope, ActionProjectType, ProjectMembershipRole, ProjectType } from "@app/db/schemas"; +import { + cryptographicOperatorPermissions, + projectAdminPermissions, + projectMemberPermissions, + projectNoAccessPermissions, + projectViewerPermission, + sshHostBootstrapPermissions +} from "@app/ee/services/permission/default-roles"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError } from "@app/lib/errors"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TRoleScopeFactory } from "../role-types"; + +type TProjectRoleScopeFactoryDep = { + permissionService: Pick; + projectDAL: Pick; +}; + +export const newProjectRoleFactory = ({ + permissionService, + projectDAL +}: TProjectRoleScopeFactoryDep): TRoleScopeFactory => { + const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const isCustomRole: TRoleScopeFactory["isCustomRole"] = (role: string) => isCustomProjectRole(role); + + const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role); + }; + + const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); + }; + + const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); + }; + + const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + }; + + const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + }; + + const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + }; + + const getPredefinedRoles: TRoleScopeFactory["getPredefinedRoles"] = async (scopeData) => { + const scope = getScopeField(scopeData); + const project = await projectDAL.findById(scope.value); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + return [ + { + id: uuidv4(), + name: "Admin", + slug: ProjectMembershipRole.Admin, + permissions: projectAdminPermissions, + description: "Full administrative access over a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId + }, + { + id: uuidv4(), + name: "Developer", + slug: ProjectMembershipRole.Member, + permissions: projectMemberPermissions, + description: "Limited read/write role in a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId + }, + { + id: uuidv4(), + name: "SSH Host Bootstrapper", + slug: ProjectMembershipRole.SshHostBootstrapper, + permissions: sshHostBootstrapPermissions, + description: "Create and issue SSH Hosts in a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId, + type: ProjectType.SSH + }, + { + id: uuidv4(), + name: "Cryptographic Operator", + slug: ProjectMembershipRole.KmsCryptographicOperator, + permissions: cryptographicOperatorPermissions, + description: "Perform cryptographic operations, such as encryption and signing, in a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId, + type: ProjectType.KMS + }, + { + id: uuidv4(), + name: "Viewer", + slug: ProjectMembershipRole.Viewer, + permissions: projectViewerPermission, + description: "Only read role in a project", + createdAt: new Date(), + projectId, + updatedAt: new Date() + }, + { + id: uuidv4(), + name: "No Access", + slug: ProjectMembershipRole.NoAccess, + permissions: projectNoAccessPermissions, + description: "No access to any resources in the project", + createdAt: new Date(), + projectId, + updatedAt: new Date() + } + ].filter(({ type }) => (type ? type === project.type : true)); + }; + + return { + onCreateRoleGuard, + onUpdateRoleGuard, + onDeleteRoleGuard, + onListRoleGuard, + onGetRoleByIdGuard, + onGetRoleBySlugGuard, + getScopeField, + getPredefinedRoles, + isCustomRole + }; +}; diff --git a/backend/src/services/role/role-dal.ts b/backend/src/services/role/role-dal.ts new file mode 100644 index 0000000000..9f3d7cf610 --- /dev/null +++ b/backend/src/services/role/role-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TRoleDALFactory = ReturnType; + +export const roleDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Role); + return orm; +}; diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts new file mode 100644 index 0000000000..41c825b2e9 --- /dev/null +++ b/backend/src/services/role/role-service.ts @@ -0,0 +1,276 @@ +import { packRules } from "@casl/ability/extra"; +import { requestContext } from "@fastify/request-context"; + +import { AccessScope, ActionProjectType, TableName } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { UnpackedPermissionSchema, unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; + +import { ActorType } from "../auth/auth-type"; +import { TExternalGroupOrgRoleMappingDALFactory } from "../external-group-org-role-mapping/external-group-org-role-mapping-dal"; +import { TIdentityDALFactory } from "../identity/identity-dal"; +import { TProjectDALFactory } from "../project/project-dal"; +import { TUserDALFactory } from "../user/user-dal"; +import { newNamespaceRoleFactory } from "./namespace/namespace-role-factory"; +import { newOrgRoleFactory } from "./org/org-role-factory"; +import { newProjectRoleFactory } from "./project/project-role-factory"; +import { TRoleDALFactory } from "./role-dal"; +import { + TCreateRoleDTO, + TDeleteRoleDTO, + TGetRoleByIdDTO, + TGetRoleBySlugDTO, + TGetUserPermissionDTO, + TListRoleDTO, + TUpdateRoleDTO +} from "./role-types"; + +type TRoleServiceFactoryDep = { + roleDAL: TRoleDALFactory; + identityDAL: Pick; + userDAL: Pick; + permissionService: Pick; + projectDAL: Pick; + externalGroupOrgRoleMappingDAL: Pick; +}; + +export type TRoleServiceFactory = ReturnType; + +export const roleServiceFactory = ({ + roleDAL, + permissionService, + projectDAL, + identityDAL, + userDAL, + externalGroupOrgRoleMappingDAL +}: TRoleServiceFactoryDep) => { + const orgRoleFactory = newOrgRoleFactory({ + permissionService, + externalGroupOrgRoleMappingDAL + }); + const projectRoleFactory = newProjectRoleFactory({ + permissionService, + projectDAL + }); + const namespaceRoleFactory = newNamespaceRoleFactory({ + permissionService + }); + const scopeFactory = { + [AccessScope.Organization]: orgRoleFactory, + [AccessScope.Project]: projectRoleFactory, + [AccessScope.Namespace]: namespaceRoleFactory + }; + + const createRole = async (dto: TCreateRoleDTO) => { + const { data, scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onCreateRoleGuard(dto); + + const scope = factory.getScopeField(scopeData); + const existingRole = await roleDAL.findOne({ + slug: data.slug, + [scope.key]: scope.value + }); + if (existingRole) throw new NotFoundError({ message: `Role with ${data.slug} exists` }); + + validateHandlebarTemplate("Role Creation", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + const role = await roleDAL.create({ + name: data.name, + description: data.description, + slug: data.slug, + permissions: data.permissions, + [scope.key]: scope.value + }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const updateRole = async (dto: TUpdateRoleDTO) => { + const { data, scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + const scope = factory.getScopeField(scopeData); + + await factory.onUpdateRoleGuard(dto); + + const existingRole = await roleDAL.findOne({ + id: dto.selector.id, + [scope.key]: scope.value + }); + if (!existingRole) throw new NotFoundError({ message: `Role with ${dto.selector.id} not found` }); + + if (data.slug) { + const existingSlug = await roleDAL.findOne({ + slug: data.slug, + [scope.key]: scope.value + }); + if (existingSlug && existingRole.id !== existingSlug.id) + throw new BadRequestError({ message: `Role with ${data.slug} already exists` }); + } + + validateHandlebarTemplate("Role Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + const role = await roleDAL.updateById(existingRole.id, { + name: data?.name, + description: data?.description, + slug: data?.slug, + permissions: data?.permissions + }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const deleteRole = async (dto: TDeleteRoleDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + const scope = factory.getScopeField(scopeData); + await factory.onDeleteRoleGuard(dto); + + const existingRole = await roleDAL.findOne({ + id: dto.selector.id, + [scope.key]: scope.value + }); + if (!existingRole) throw new NotFoundError({ message: `Role with ${dto.selector.id} not found` }); + + const [role] = await roleDAL.delete({ + id: existingRole.id, + [scope.key]: scope.value + }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const listRoles = async (dto: TListRoleDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListRoleGuard(dto); + + const scope = factory.getScopeField(scopeData); + const predefinedRoles = await factory.getPredefinedRoles(scopeData); + const roles = await roleDAL.find( + { + [scope.key]: scope.value + }, + { limit: dto.data.limit, offset: dto.data.offset, sort: [[`${TableName.Role}.slug` as "slug", "asc"]] } + ); + + return { + roles: [...predefinedRoles, ...roles.map((el) => ({ ...el, permissions: unpackPermissions(el.permissions) }))] + }; + }; + + const getRoleById = async (dto: TGetRoleByIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetRoleByIdGuard(dto); + + const scope = factory.getScopeField(scopeData); + + const predefinedRole = await factory.getPredefinedRoles(scopeData); + const selectedRole = predefinedRole.find((el) => el.id === dto.selector.id); + if (selectedRole) { + return { ...selectedRole, permissions: UnpackedPermissionSchema.array().parse(selectedRole.permissions) }; + } + + const role = await roleDAL.findOne({ + id: selector.id, + [scope.key]: scope.value + }); + if (!role) throw new NotFoundError({ message: `Role with id ${dto.selector.id} not found` }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const getRoleBySlug = async (dto: TGetRoleBySlugDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetRoleBySlugGuard(dto); + + const scope = factory.getScopeField(scopeData); + const isCustomRole = factory.isCustomRole(dto.selector.slug); + if (!isCustomRole) { + const predefinedRole = await factory.getPredefinedRoles(scopeData); + const selectedRole = predefinedRole.find((el) => el.slug === dto.selector.slug); + if (!selectedRole) throw new BadRequestError({ message: `Role with slug ${dto.selector.slug} not found` }); + return { ...selectedRole, permissions: UnpackedPermissionSchema.array().parse(selectedRole.permissions) }; + } + + const role = await roleDAL.findOne({ + slug: selector.slug, + [scope.key]: scope.value + }); + if (!role) throw new NotFoundError({ message: `Role with slug ${dto.selector.slug} not found` }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const getUserPermission = async (dto: TGetUserPermissionDTO) => { + if (dto.scopeData.scope === AccessScope.Organization) { + const { permission, memberships } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails: undefined }; + } + + if (dto.scopeData.scope === AccessScope.Project) { + const { permission, memberships } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: dto.scopeData.projectId, + actorOrgId: dto.permission.orgId + }); + + const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails"); + const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === dto.scopeData.projectId; + const assumedPrivilegeDetails = isAssumingPrivilege + ? { + actorId: assumedPrivilegeDetailsCtx?.actorId, + actorType: assumedPrivilegeDetailsCtx?.actorType, + actorName: "", + actorEmail: "" + } + : undefined; + + if (assumedPrivilegeDetails?.actorType === ActorType.IDENTITY) { + const identityDetails = await identityDAL.findById(assumedPrivilegeDetails.actorId); + if (!identityDetails) + throw new NotFoundError({ message: `Identity with ID ${assumedPrivilegeDetails.actorId} not found` }); + assumedPrivilegeDetails.actorName = identityDetails.name; + } else if (assumedPrivilegeDetails?.actorType === ActorType.USER) { + const userDetails = await userDAL.findById(assumedPrivilegeDetails?.actorId); + if (!userDetails) + throw new NotFoundError({ message: `User with ID ${assumedPrivilegeDetails.actorId} not found` }); + assumedPrivilegeDetails.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`; + assumedPrivilegeDetails.actorEmail = userDetails?.email || ""; + } + + return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails }; + } + + throw new BadRequestError({ message: "Invalid scope defined" }); + }; + + return { + createRole, + updateRole, + deleteRole, + listRoles, + getRoleById, + getRoleBySlug, + getUserPermission + }; +}; diff --git a/backend/src/services/role/role-types.ts b/backend/src/services/role/role-types.ts new file mode 100644 index 0000000000..96fc9c0f35 --- /dev/null +++ b/backend/src/services/role/role-types.ts @@ -0,0 +1,79 @@ +import { MongoAbility, RawRuleOf } from "@casl/ability"; + +import { AccessScopeData, TRoles } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TRoleScopeFactory { + onCreateRoleGuard: (arg: TCreateRoleDTO) => Promise; + onUpdateRoleGuard: (arg: TUpdateRoleDTO) => Promise; + onDeleteRoleGuard: (arg: TDeleteRoleDTO) => Promise; + onListRoleGuard: (arg: TListRoleDTO) => Promise; + getPredefinedRoles: (arg: AccessScopeData) => Promise<(TRoles & { permissions: RawRuleOf[] })[]>; + onGetRoleByIdGuard: (arg: TGetRoleByIdDTO) => Promise; + onGetRoleBySlugGuard: (arg: TGetRoleBySlugDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + name: string; + description?: string | null; + slug: string; + permissions: unknown; + }; +}; + +export type TUpdateRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + }; + data: Partial<{ + name: string; + description?: string | null; + slug: string; + permissions: unknown; + }>; +}; + +export type TListRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + limit?: number; + offset?: number; + }; +}; + +export type TDeleteRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + }; +}; + +export type TGetRoleByIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + }; +}; + +export type TGetRoleBySlugDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + slug: string; + }; +}; + +export type TGetUserPermissionDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; +}; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 30cd932912..36f3459cfb 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -385,7 +385,7 @@ export const secretImportServiceFactory = ({ path: secretPath, id: secretImportDocId }: TResyncSecretImportReplicationDTO) => { - const { permission, membership } = await permissionService.getProjectPermission({ + const { permission, memberships } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -437,7 +437,7 @@ export const secretImportServiceFactory = ({ secretImportDoc.importPath ); - if (membership && sourceFolder) { + if (memberships?.length && sourceFolder) { await secretQueueService.replicateSecrets({ orgId: actorOrgId, secretPath: secretImportDoc.importPath, diff --git a/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts b/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts index ec4a3f807d..384de47e8d 100644 --- a/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts +++ b/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { AccessScope, TableName } from "@app/db/schemas"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TSecretReminderRecipientsDALFactory = ReturnType; @@ -14,13 +14,13 @@ export const secretReminderRecipientsDALFactory = (db: TDbClient) => { .where({ secretId }) .leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.Project, `${TableName.SecretReminderRecipients}.projectId`, `${TableName.Project}.id`) - .leftJoin(TableName.OrgMembership, (bd) => { + .leftJoin(TableName.Membership, (bd) => { void bd - .on(`${TableName.OrgMembership}.userId`, "=", `${TableName.SecretReminderRecipients}.userId`) - .andOn(`${TableName.OrgMembership}.orgId`, "=", `${TableName.Project}.orgId`); + .on(`${TableName.Membership}.actorUserId`, "=", `${TableName.SecretReminderRecipients}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, "=", `${TableName.Project}.orgId`) + .andOn(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Organization])); }) - - .where(`${TableName.OrgMembership}.isActive`, true) + .where(`${TableName.Membership}.isActive`, true) .select(selectAllTableCols(TableName.SecretReminderRecipients)) .select( db.ref("email").withSchema(TableName.Users).as("email"), diff --git a/backend/src/services/secret-sync/railway/railway-sync-fns.ts b/backend/src/services/secret-sync/railway/railway-sync-fns.ts index 07862aeb5c..731691acfd 100644 --- a/backend/src/services/secret-sync/railway/railway-sync-fns.ts +++ b/backend/src/services/secret-sync/railway/railway-sync-fns.ts @@ -12,6 +12,8 @@ export const RailwaySyncFns = { async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise { try { const config = secretSync.destinationConfig; + const { keySchema } = secretSync.syncOptions; + const { environment } = secretSync; const variables = await RailwayPublicAPI.getVariables(secretSync.connection, { projectId: config.projectId, @@ -26,6 +28,10 @@ export const RailwaySyncFns = { // eslint-disable-next-line no-continue if (key.startsWith("RAILWAY_")) continue; + // Check if key matches the schema + // eslint-disable-next-line no-continue + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + entries[key] = { value }; @@ -40,60 +46,73 @@ export const RailwaySyncFns = { } }, + /** + * Syncs secrets to Railway and redeploys the service if needed. + * + * Gets existing Railway vars, merges with new secrets (keeping Railway vars if deletion is disabled), + * then replaces every variable with the new values, if variable is not in the secretMap, it is deleted. + * If there's a service, triggers a redeploy to pick up the changes. + */ async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) { - const { - environment, - syncOptions: { disableSecretDeletion, keySchema } - } = secretSync; - const railwaySecrets = await this.getSecrets(secretSync); - const config = secretSync.destinationConfig; + try { + const { + syncOptions: { disableSecretDeletion } + } = secretSync; + const railwaySecrets = await this.getSecrets(secretSync); + const config = secretSync.destinationConfig; - for await (const key of Object.keys(secretMap)) { - try { - const existing = railwaySecrets[key]; + const railwaySecretsMap = Object.fromEntries( + Object.entries(railwaySecrets).map(([key, secret]) => [key, secret.value]) + ); + const secretMapMap = Object.fromEntries(Object.entries(secretMap).map(([key, secret]) => [key, secret.value])); - if (existing === undefined || existing.value !== secretMap[key].value) { - await RailwayPublicAPI.upsertVariable(secretSync.connection, { - input: { - projectId: config.projectId, - environmentId: config.environmentId, - serviceId: config.serviceId || undefined, - name: key, - value: secretMap[key].value ?? "" - } - }); + const toReplace = disableSecretDeletion ? { ...railwaySecretsMap, ...secretMapMap } : secretMapMap; + + const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, { + input: { + projectId: config.projectId, + environmentId: config.environmentId, + serviceId: config.serviceId || undefined, + skipDeploys: true, + variables: toReplace, + replace: true } - } catch (error) { + }); + + if (!upserted) throw new SecretSyncError({ - error, - secretKey: key + message: "Failed to upsert secrets to Railway" }); - } - } - if (disableSecretDeletion) return; + if (!config.serviceId) return; - for await (const key of Object.keys(railwaySecrets)) { - try { - // eslint-disable-next-line no-continue - if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + const latestDeployment = await RailwayPublicAPI.getDeployments(secretSync.connection, { + input: { + serviceId: config.serviceId, + environmentId: config.environmentId + }, + first: 1 + }); - if (!secretMap[key]) { - await RailwayPublicAPI.deleteVariable(secretSync.connection, { - input: { - projectId: config.projectId, - environmentId: config.environmentId, - serviceId: config.serviceId || undefined, - name: key - } - }); + const latestDeploymentId = latestDeployment?.deployments.edges[0].node.id; + + if (!latestDeploymentId) + throw new SecretSyncError({ + message: "Failed to get latest deployment from Railway" + }); + + await RailwayPublicAPI.redeployDeployment(secretSync.connection, { + input: { + deploymentId: latestDeploymentId } - } catch (error) { - throw new SecretSyncError({ - error, - secretKey: key - }); - } + }); + } catch (error) { + if (error instanceof SecretSyncError) throw error; + + throw new SecretSyncError({ + error, + message: "Failed to sync secrets to Railway" + }); } }, @@ -101,24 +120,37 @@ export const RailwaySyncFns = { const existing = await this.getSecrets(secretSync); const config = secretSync.destinationConfig; - for await (const secret of Object.keys(existing)) { - try { - if (secret in secretMap) { - await RailwayPublicAPI.deleteVariable(secretSync.connection, { - input: { - projectId: config.projectId, - environmentId: config.environmentId, - serviceId: config.serviceId || undefined, - name: secret - } - }); + // Create a new variables object excluding secrets that exist in secretMap + const remainingVariables = Object.fromEntries( + Object.entries(existing) + .filter(([key]) => !(key in secretMap)) + .map(([key, secret]) => [key, secret.value]) + ); + + try { + const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, { + input: { + projectId: config.projectId, + environmentId: config.environmentId, + serviceId: config.serviceId || undefined, + skipDeploys: true, + variables: remainingVariables, + replace: true } - } catch (error) { + }); + + if (!upserted) { throw new SecretSyncError({ - error, - secretKey: secret + message: "Failed to remove secrets from Railway" }); } + } catch (error) { + if (error instanceof SecretSyncError) throw error; + + throw new SecretSyncError({ + error, + message: "Failed to remove secrets from Railway" + }); } } }; diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts index 57c6581ced..64d2c0bdb2 100644 --- a/backend/src/services/secret-sync/secret-sync-dal.ts +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -204,5 +204,19 @@ export const secretSyncDALFactory = ( } }; - return { ...secretSyncOrm, findById, findOne, find, create, updateById }; + const findByDestinationAndOrgId = async (destination: string, orgId: string, tx?: Knex) => { + try { + const response = await (tx || db.replicaNode())(TableName.SecretSync) + .join(TableName.Project, `${TableName.SecretSync}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.SecretSync}.destination`, destination) + .where(`${TableName.Project}.orgId`, orgId) + .select(selectAllTableCols(TableName.SecretSync)); + + return response; + } catch (error) { + throw new DatabaseError({ error, name: "Find By Destination And Org ID - Secret Sync" }); + } + }; + + return { ...secretSyncOrm, findById, findOne, find, create, updateById, findByDestinationAndOrgId }; }; diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 04e91051da..1fbc66ccab 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -1,5 +1,6 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { SecretSync, SecretSyncPlanType } from "@app/services/secret-sync/secret-sync-enums"; +import { DestinationDuplicateCheckFn } from "@app/services/secret-sync/secret-sync-types"; export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", @@ -99,3 +100,104 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Netlify]: SecretSyncPlanType.Regular, [SecretSync.Bitbucket]: SecretSyncPlanType.Regular }; + +export const SECRET_SYNC_SKIP_FIELDS_MAP: Record = { + [SecretSync.AWSParameterStore]: [], + [SecretSync.AWSSecretsManager]: ["mappingBehavior", "secretName"], + [SecretSync.GitHub]: [], + [SecretSync.GCPSecretManager]: [], + [SecretSync.AzureKeyVault]: [], + [SecretSync.AzureAppConfiguration]: ["label"], + [SecretSync.AzureDevOps]: ["devopsProjectName"], + [SecretSync.Databricks]: [], + [SecretSync.Humanitec]: [], + [SecretSync.TerraformCloud]: ["variableSetName", "workspaceName"], + [SecretSync.Camunda]: [], + [SecretSync.Vercel]: ["appName"], + [SecretSync.Windmill]: [], + [SecretSync.HCVault]: [], + [SecretSync.TeamCity]: [], + [SecretSync.OCIVault]: [], + [SecretSync.OnePass]: ["valueLabel"], + [SecretSync.Heroku]: ["appName"], + [SecretSync.Render]: [], + [SecretSync.Flyio]: [], + [SecretSync.GitLab]: [ + "projectName", + "shouldProtectSecrets", + "shouldMaskSecrets", + "shouldHideSecrets", + "targetEnvironment", + "groupName", + "groupId", + "projectId" + ], + [SecretSync.CloudflarePages]: [], + [SecretSync.CloudflareWorkers]: [], + [SecretSync.Supabase]: ["projectName"], + [SecretSync.Zabbix]: ["hostName", "macroType"], + [SecretSync.Railway]: ["projectName", "environmentName", "serviceName"], + [SecretSync.Checkly]: ["groupName", "accountName"], + [SecretSync.DigitalOceanAppPlatform]: ["appName"], + [SecretSync.Netlify]: ["accountName", "siteName"], + [SecretSync.Bitbucket]: [] +}; + +const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true; + +export const DESTINATION_DUPLICATE_CHECK_MAP: Record = { + [SecretSync.AWSParameterStore]: defaultDuplicateCheck, + [SecretSync.AWSSecretsManager]: defaultDuplicateCheck, + [SecretSync.GitHub]: defaultDuplicateCheck, + [SecretSync.GCPSecretManager]: defaultDuplicateCheck, + [SecretSync.AzureKeyVault]: defaultDuplicateCheck, + [SecretSync.AzureAppConfiguration]: defaultDuplicateCheck, + [SecretSync.AzureDevOps]: defaultDuplicateCheck, + [SecretSync.Databricks]: defaultDuplicateCheck, + [SecretSync.Humanitec]: defaultDuplicateCheck, + [SecretSync.TerraformCloud]: defaultDuplicateCheck, + [SecretSync.Camunda]: defaultDuplicateCheck, + [SecretSync.Vercel]: defaultDuplicateCheck, + [SecretSync.Windmill]: defaultDuplicateCheck, + [SecretSync.HCVault]: defaultDuplicateCheck, + [SecretSync.TeamCity]: defaultDuplicateCheck, + [SecretSync.OCIVault]: defaultDuplicateCheck, + [SecretSync.OnePass]: defaultDuplicateCheck, + [SecretSync.Heroku]: defaultDuplicateCheck, + [SecretSync.Render]: defaultDuplicateCheck, + [SecretSync.Flyio]: defaultDuplicateCheck, + [SecretSync.GitLab]: (existingConfig, newConfig) => { + const existingTargetEnv = existingConfig.targetEnvironment as string | undefined; + const newTargetEnv = newConfig.targetEnvironment as string | undefined; + + const wildcardValues = ["*", ""]; + + if ( + (newConfig.scope as string) === "group" + ? existingConfig.groupId !== newConfig.groupId + : existingConfig.projectId !== newConfig.projectId + ) + return false; + + // If either has wildcard, it conflicts with any targetEnvironment + if ( + !existingTargetEnv || + !newTargetEnv || + wildcardValues.includes(existingTargetEnv) || + wildcardValues.includes(newTargetEnv) + ) { + return true; + } + + return existingTargetEnv === newTargetEnv; + }, + [SecretSync.CloudflarePages]: defaultDuplicateCheck, + [SecretSync.CloudflareWorkers]: defaultDuplicateCheck, + [SecretSync.Supabase]: defaultDuplicateCheck, + [SecretSync.Zabbix]: defaultDuplicateCheck, + [SecretSync.Railway]: defaultDuplicateCheck, + [SecretSync.Checkly]: defaultDuplicateCheck, + [SecretSync.DigitalOceanAppPlatform]: defaultDuplicateCheck, + [SecretSync.Netlify]: defaultDuplicateCheck, + [SecretSync.Bitbucket]: defaultDuplicateCheck +}; diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index ecd7d04a50..6a2f49386c 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -12,6 +12,7 @@ import { import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { deepEqualSkipFields } from "@app/lib/fn/object"; import { OrgServiceActor } from "@app/lib/types"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; @@ -20,6 +21,7 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { enterpriseSyncCheck, listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncStatus, + TCheckDuplicateDestinationDTO, TCreateSecretSyncDTO, TDeleteSecretSyncDTO, TFindSecretSyncByIdDTO, @@ -35,7 +37,12 @@ import { import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretSyncDALFactory } from "./secret-sync-dal"; -import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps"; +import { + DESTINATION_DUPLICATE_CHECK_MAP, + SECRET_SYNC_CONNECTION_MAP, + SECRET_SYNC_NAME_MAP, + SECRET_SYNC_SKIP_FIELDS_MAP +} from "./secret-sync-maps"; import { TSecretSyncQueueFactory } from "./secret-sync-queue"; type TSecretSyncServiceFactoryDep = { @@ -696,6 +703,61 @@ export const secretSyncServiceFactory = ({ return updatedSecretSync as TSecretSync; }; + const checkDuplicateDestination = async ( + { destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO, + actor: OrgServiceActor + ) => { + const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination]; + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSub.SecretSyncs + ); + + if (!destinationConfig || Object.keys(destinationConfig).length === 0) { + return { hasDuplicate: false, duplicateProjectId: undefined }; + } + + try { + const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId); + + const duplicates = existingSyncs.filter((sync) => { + if (sync.id === excludeSyncId) { + return false; + } + + try { + const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields); + if (baseFieldsMatch) { + return DESTINATION_DUPLICATE_CHECK_MAP[destination]( + sync.destinationConfig as Record, + destinationConfig + ); + } + return false; + } catch { + return false; + } + }); + + const hasDuplicate = duplicates.length > 0; + return { + hasDuplicate, + duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined + }; + } catch (error) { + return { hasDuplicate: false, duplicateProjectId: undefined }; + } + }; + return { listSecretSyncOptions, listSecretSyncsByProjectId, @@ -707,6 +769,7 @@ export const secretSyncServiceFactory = ({ deleteSecretSync, triggerSecretSyncSyncSecretsById, triggerSecretSyncImportSecretsById, - triggerSecretSyncRemoveSecretsById + triggerSecretSyncRemoveSecretsById, + checkDuplicateDestination }; }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 6435e19d39..478c44c92a 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -324,6 +324,13 @@ export type TDeleteSecretSyncDTO = { removeSecrets: boolean; }; +export type TCheckDuplicateDestinationDTO = { + destination: SecretSync; + destinationConfig: Record; + excludeSyncId?: string; + projectId: string; +}; + export enum SecretSyncStatus { Pending = "pending", Running = "running", @@ -408,3 +415,8 @@ export type TSecretMap = Record< secretMetadata?: ResourceMetadataDTO; } >; + +export type DestinationDuplicateCheckFn = ( + existingConfig: Record, + newConfig: Record +) => boolean; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index de8f930ed3..579b19b19b 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2395,8 +2395,8 @@ export const secretV2BridgeServiceFactory = ({ projectId: folder.projectId, secretVersions: secretVersionsFilter, findOpt: { - offset, limit, + offset, sort: [["createdAt", "desc"]] } }); diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 0282fa5372..7d25dac864 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -2,7 +2,13 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; +import { + AccessScope, + SecretVersionsV2Schema, + TableName, + TSecretVersionsV2, + TSecretVersionsV2Update +} from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; @@ -191,11 +197,11 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt; const query = (tx || db.replicaNode())(TableName.SecretVersionV2) .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) - .leftJoin( - TableName.ProjectMembership, - `${TableName.ProjectMembership}.userId`, - `${TableName.SecretVersionV2}.userActorId` - ) + .leftJoin(TableName.Membership, (qb) => { + void qb + .on(`${TableName.Membership}.actorUserId`, `${TableName.SecretVersionV2}.userActorId`) + .andOn(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Project])); + }) .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) .leftJoin( @@ -210,19 +216,19 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ) .where((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.where(`${TableName.ProjectMembership}.projectId`, projectId); + void qb.where(`${TableName.Membership}.scopeProjectId`, projectId); if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); }) .orWhere((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.whereNull(`${TableName.ProjectMembership}.projectId`); + void qb.whereNull(`${TableName.Membership}.scopeProjectId`); if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); }) .select( selectAllTableCols(TableName.SecretVersionV2), db.ref("username").withSchema(TableName.Users).as("userActorName"), db.ref("name").withSchema(TableName.Identity).as("identityActorName"), - db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Membership).as("membershipId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index ed794a664e..61507d1275 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -4,6 +4,7 @@ import { AxiosError } from "axios"; import { Knex } from "knex"; import { + AccessScope, ProjectMembershipRole, ProjectType, ProjectUpgradeStatus, @@ -43,6 +44,8 @@ import { TIntegrationAuthServiceFactory } from "../integration-auth/integration- import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipDALFactory } from "../membership/membership-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectDALFactory } from "../project/project-dal"; import { createProjectKey } from "../project/project-fns"; @@ -50,7 +53,6 @@ import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; @@ -92,7 +94,9 @@ type TSecretQueueFactoryDep = { projectDAL: TProjectDALFactory; projectBotDAL: TProjectBotDALFactory; projectKeyDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: Pick; + membershipRoleDAL: Pick; smtpService: TSmtpService; secretVersionDAL: TSecretVersionDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; @@ -110,7 +114,6 @@ type TSecretQueueFactoryDep = { keyStore: Pick; auditLogService: Pick; orgService: Pick; - projectUserMembershipRoleDAL: Pick; resourceMetadataDAL: Pick; folderCommitService: Pick; secretSyncQueue: Pick; @@ -173,14 +176,15 @@ export const secretQueueFactory = ({ keyStore, auditLogService, orgService, - projectUserMembershipRoleDAL, projectKeyDAL, resourceMetadataDAL, secretSyncQueue, folderCommitService, reminderService, eventBusService, - licenseService + licenseService, + membershipUserDAL, + membershipRoleDAL }: TSecretQueueFactoryDep) => { const integrationMeter = opentelemetry.metrics.getMeter("Integrations"); const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", { @@ -1165,17 +1169,16 @@ export const secretQueueFactory = ({ // if project v1 create the project ghost user if (project.version === ProjectVersion.V1) { const ghostUser = await orgService.addGhostUser(project.orgId, tx); - const projectMembership = await projectMembershipDAL.create( + const projectMembership = await membershipUserDAL.create( { - userId: ghostUser.user.id, - projectId: project.id + actorUserId: ghostUser.user.id, + scopeOrgId: project.orgId, + scope: AccessScope.Project, + scopeProjectId: project.id }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); + await membershipRoleDAL.create({ membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, tx); const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ publicKey: ghostUser.keys.publicKey, diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 3a5ccc6672..723cd368cc 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1283,7 +1283,8 @@ export const secretServiceFactory = ({ }); const { userPermissions, identityPermissions, groupPermissions } = await permissionService.getProjectPermissions( - dto.projectId + dto.projectId, + dto.actorOrgId ); const attachAllowedActions = ( diff --git a/backend/src/services/smtp/emails/AccountDeletionConfirmationTemplate.tsx b/backend/src/services/smtp/emails/AccountDeletionConfirmationTemplate.tsx new file mode 100644 index 0000000000..ff4cb7de92 --- /dev/null +++ b/backend/src/services/smtp/emails/AccountDeletionConfirmationTemplate.tsx @@ -0,0 +1,36 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface AccountDeletionConfirmationTemplateProps + extends Omit { + email: string; +} + +export const AccountDeletionConfirmationTemplate = ({ email, siteUrl }: AccountDeletionConfirmationTemplateProps) => { + return ( + + + Account Deleted + +
+ + This email confirms that your Infisical account {email} has been deleted, including all + associated data. + +
+
+ ); +}; + +export default AccountDeletionConfirmationTemplate; + +AccountDeletionConfirmationTemplate.PreviewProps = { + email: "test@infisical.com", + siteUrl: "https://infisical.com" +} as AccountDeletionConfirmationTemplateProps; diff --git a/backend/src/services/smtp/emails/HealthAlertTemplate.tsx b/backend/src/services/smtp/emails/HealthAlertTemplate.tsx new file mode 100644 index 0000000000..46b919f113 --- /dev/null +++ b/backend/src/services/smtp/emails/HealthAlertTemplate.tsx @@ -0,0 +1,47 @@ +import { Heading, Section, Text } from "@react-email/components"; + +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; + +interface HealthAlertTemplateProps extends Omit { + type: "gateway" | "relay" | "instance-relay"; + names: string; +} + +export const HealthAlertTemplate = ({ siteUrl, names, type }: HealthAlertTemplateProps) => { + return ( + + + {type === "gateway" ? "Gateway" : "Relay"} Health Alert + +
+ + The following {type}(s) in your organization may be offline as they haven't reported a + heartbeat in over an hour: {names}. + + + {type === "instance-relay" && ( + <> + If the issue persists, you can contact the Infisical team at{" "} + support@infisical.com. + + )} + {type === "relay" && <>Please contact your relay administrators.} + {type === "gateway" && <>Please contact your gateway administrators.} + +
+
+ ); +}; + +export default HealthAlertTemplate; + +HealthAlertTemplate.PreviewProps = { + type: "gateway", + names: '"gateway1", "gateway2"', + siteUrl: "https://infisical.com" +} as HealthAlertTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 066744596e..06ac31ab69 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -1,10 +1,12 @@ export * from "./AccessApprovalRequestTemplate"; export * from "./AccessApprovalRequestUpdatedTemplate"; +export * from "./AccountDeletionConfirmationTemplate"; export * from "./EmailMfaTemplate"; export * from "./EmailVerificationTemplate"; export * from "./ExternalImportFailedTemplate"; export * from "./ExternalImportStartedTemplate"; export * from "./ExternalImportSucceededTemplate"; +export * from "./HealthAlertTemplate"; export * from "./IntegrationSyncFailedTemplate"; export * from "./NewDeviceLoginTemplate"; export * from "./OAuthPasswordResetTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index d64582fe00..652f565679 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -9,11 +9,13 @@ import { logger } from "@app/lib/logger"; import { AccessApprovalRequestTemplate, AccessApprovalRequestUpdatedTemplate, + AccountDeletionConfirmationTemplate, EmailMfaTemplate, EmailVerificationTemplate, ExternalImportFailedTemplate, ExternalImportStartedTemplate, ExternalImportSucceededTemplate, + HealthAlertTemplate, IntegrationSyncFailedTemplate, NewDeviceLoginTemplate, OAuthPasswordResetTemplate, @@ -83,7 +85,9 @@ export enum SmtpTemplates { OrgAdminBreakglassAccess = "orgAdminBreakglassAccess", ServiceTokenExpired = "serviceTokenExpired", SecretScanningV2ScanFailed = "secretScanningV2ScanFailed", - SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected" + SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected", + AccountDeletionConfirmation = "accountDeletionConfirmation", + HealthAlert = "healthAlert" } export enum SmtpHost { @@ -128,7 +132,9 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.SetupPassword]: PasswordSetupTemplate, [SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate, [SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate, - [SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate + [SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate, + [SmtpTemplates.AccountDeletionConfirmation]: AccountDeletionConfirmationTemplate, + [SmtpTemplates.HealthAlert]: HealthAlertTemplate }; export const smtpServiceFactory = (cfg: TSmtpConfig) => { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b43e4e0a27..84a53f4075 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,7 @@ import { CronJob } from "cron"; import { + AccessScope, IdentityAuthMethod, OrgMembershipRole, OrgMembershipStatus, @@ -29,7 +30,6 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityTokenAuthDALFactory } from "../identity-token-auth/identity-token-auth-dal"; @@ -37,10 +37,12 @@ import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { RootKeyEncryptionStrategy } from "../kms/kms-types"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; -import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; @@ -64,16 +66,17 @@ type TSuperAdminServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityTokenAuthDAL: TIdentityTokenAuthDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; - identityOrgMembershipDAL: TIdentityOrgDALFactory; orgDAL: TOrgDALFactory; - orgMembershipDAL: TOrgMembershipDALFactory; serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; + membershipUserDAL: TMembershipUserDALFactory; + membershipIdentityDAL: TMembershipIdentityDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; userAliasDAL: Pick; authService: Pick; kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; - orgService: Pick; + orgService: Pick; keyStore: Pick; licenseService: Pick; microsoftTeamsService: Pick; @@ -127,7 +130,6 @@ export const superAdminServiceFactory = ({ userDAL, identityDAL, orgDAL, - orgMembershipDAL, userAliasDAL, authService, orgService, @@ -137,11 +139,13 @@ export const superAdminServiceFactory = ({ licenseService, identityAccessTokenDAL, identityTokenAuthDAL, - identityOrgMembershipDAL, microsoftTeamsService, invalidateCacheQueue, smtpService, - tokenService + tokenService, + membershipIdentityDAL, + membershipUserDAL, + membershipRoleDAL }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -589,10 +593,17 @@ export const superAdminServiceFactory = ({ const { identity, credentials } = await identityDAL.transaction(async (tx) => { const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); - await identityOrgMembershipDAL.create( + const membership = await membershipIdentityDAL.create( { - identityId: newIdentity.id, - orgId: organization.id, + actorIdentityId: newIdentity.id, + scopeOrgId: organization.id, + scope: AccessScope.Organization + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, role: OrgMembershipRole.Admin }, tx @@ -741,13 +752,12 @@ export const superAdminServiceFactory = ({ }; const getOrganizations = async ({ offset, limit, searchTerm }: TGetOrganizationsDTO) => { - const organizations = await orgDAL.findOrganizationsByFilter({ + return orgDAL.findOrganizationsByFilter({ offset, searchTerm, sortBy: "name", limit }); - return organizations; }; const createOrganization = async ( @@ -835,18 +845,26 @@ export const superAdminServiceFactory = ({ }); } - await orgDAL.createMembership( + const membership = await orgDAL.createMembership( { - userId: inviteeUser.id, + actorUserId: inviteeUser.id, + scope: AccessScope.Organization, inviteEmail: inviteeEmail, - orgId: org.id, - role: OrgMembershipRole.Admin, + scopeOrgId: org.id, status: inviteeUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); + users.push(inviteeUser); } @@ -915,23 +933,32 @@ export const superAdminServiceFactory = ({ actorType: ActorType ) => { if (actorType === ActorType.USER) { - const orgMembership = await orgMembershipDAL.findById(membershipId); + const orgMembership = await membershipUserDAL.findOne({ + scope: AccessScope.Organization, + id: membershipId, + scopeOrgId: organizationId + }); if (!orgMembership) { throw new NotFoundError({ name: "Organization Membership", message: "Organization membership not found" }); } - if (orgMembership.userId === actorId) { + if (orgMembership.actorUserId === actorId) { throw new BadRequestError({ message: "You cannot remove yourself from the organization from the instance management panel." }); } } - const [organizationMembership] = await orgMembershipDAL.delete({ - orgId: organizationId, + const membershipRole = await membershipRoleDAL.findOne({ membershipId }); + if (!membershipRole) { + throw new NotFoundError({ name: "Membership Role", message: "Membership role not found" }); + } + const [organizationMembership] = await membershipUserDAL.delete({ + scopeOrgId: organizationId, + scope: AccessScope.Organization, id: membershipId }); - return organizationMembership; + return { ...organizationMembership, role: membershipRole.role, orgId: organizationId }; }; const joinOrganization = async (orgId: string, actor: OrgServiceActor) => { @@ -947,25 +974,46 @@ export const superAdminServiceFactory = ({ throw new NotFoundError({ message: `Could not organization with ID "${orgId}"` }); } - const existingOrgMembership = await orgMembershipDAL.findOne({ userId: serverAdmin.id, orgId }); + const existingOrgMembership = await membershipUserDAL.findOne({ + actorUserId: serverAdmin.id, + scopeOrgId: org.id, + scope: AccessScope.Organization + }); if (existingOrgMembership) { throw new BadRequestError({ message: `You are already a part of the organization with ID ${orgId}` }); } - const orgMembership = await orgDAL.createMembership({ - userId: serverAdmin.id, - orgId: org.id, - role: OrgMembershipRole.Admin, - status: OrgMembershipStatus.Accepted, - isActive: true + const orgMembership = await orgDAL.transaction(async (tx) => { + const membership = await orgDAL.createMembership( + { + actorUserId: serverAdmin.id, + scopeOrgId: org.id, + status: OrgMembershipStatus.Accepted, + isActive: true, + scope: AccessScope.Organization + }, + tx + ); + const membershipRole = await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); + return { ...membership, role: membershipRole.role, orgId: org.id }; }); return orgMembership; }; const resendOrgInvite = async ({ organizationId, membershipId }: TResendOrgInviteDTO, actor: OrgServiceActor) => { - const orgMembership = await orgMembershipDAL.findOne({ id: membershipId, orgId: organizationId }); + const orgMembership = await membershipUserDAL.findOne({ + id: membershipId, + scopeOrgId: organizationId, + scope: AccessScope.Organization + }); if (!orgMembership) { throw new NotFoundError({ name: "Organization Membership", message: "Organization membership not found" }); @@ -977,7 +1025,7 @@ export const superAdminServiceFactory = ({ }); } - if (!orgMembership.userId) { + if (!orgMembership.actorUserId) { throw new NotFoundError({ message: "Cannot find user associated with Org Membership." }); } @@ -985,15 +1033,15 @@ export const superAdminServiceFactory = ({ throw new BadRequestError({ message: "No invite email associated with user." }); } - const org = await orgDAL.findOrgById(orgMembership.orgId); + const org = await orgDAL.findOrgById(orgMembership.scopeOrgId); const appCfg = getConfig(); const serverAdmin = await userDAL.findById(actor.id); const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: orgMembership.userId, - orgId: orgMembership.orgId + userId: orgMembership.actorUserId, + orgId: orgMembership.scopeOrgId }); await smtpService.sendMail({ @@ -1005,17 +1053,17 @@ export const superAdminServiceFactory = ({ inviterUsername: serverAdmin?.email, organizationName: org?.name, email: orgMembership.inviteEmail, - organizationId: orgMembership.orgId, + organizationId: orgMembership.scopeOrgId, token, callback_url: `${appCfg.SITE_URL}/signupinvite` } }); - return orgMembership; + return { ...orgMembership, orgId: organizationId, role: "" }; }; const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { - const identities = await identityDAL.getIdentitiesByFilter({ + const result = await identityDAL.getIdentitiesByFilter({ limit, offset, searchTerm, @@ -1023,10 +1071,13 @@ export const superAdminServiceFactory = ({ }); const serverCfg = await getServerCfg(); - return identities.map((identity) => ({ - ...identity, - isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id)) - })); + return { + identities: result.identities.map((identity) => ({ + ...identity, + isInstanceAdmin: Boolean(serverCfg?.adminIdentityIds?.includes(identity.id)) + })), + total: result.total + }; }; const grantServerAdminAccessToUser = async (userId: string) => { diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 4267d13ee1..41b615b5df 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, TableName, TUserActionsInsert, TUserActionsUpdate, @@ -60,11 +61,20 @@ export const userDALFactory = (db: TDbClient) => { query = query.where("superAdmin", true); } + const countQuery = query.clone(); + if (sortBy) { query = query.orderBy(sortBy); } - return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users)); + const [users, totalResult] = await Promise.all([ + query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users)), + countQuery.count("*", { as: "count" }).first() + ]); + + const total = Number(totalResult?.count || 0); + + return { users, total }; } catch (error) { throw new DatabaseError({ error, name: "Get users by filter" }); } @@ -118,9 +128,13 @@ export const userDALFactory = (db: TDbClient) => { const findUserByProjectMembershipId = async (projectMembershipId: string) => { try { return await db - .replicaNode()(TableName.ProjectMembership) - .where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ + [`${TableName.Membership}.id` as "id"]: projectMembershipId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project + }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .first(); } catch (error) { throw new DatabaseError({ error, name: "Find user by project membership id" }); @@ -130,9 +144,11 @@ export const userDALFactory = (db: TDbClient) => { const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => { try { return await db - .replicaNode()(TableName.ProjectMembership) - .whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .whereIn(`${TableName.Membership}.id`, projectMembershipIds) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .select("*"); } catch (error) { throw new DatabaseError({ error, name: "Find users by project membership ids" }); @@ -182,8 +198,10 @@ export const userDALFactory = (db: TDbClient) => { try { const doc = await db(TableName.Users) .where({ email }) - .leftJoin(TableName.OrgMembership, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .leftJoin(TableName.Membership, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .select(selectAllTableCols(TableName.Users)) .select( db.ref("name").withSchema(TableName.Organization).as("orgName"), diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b8058f327f..b54eab8eff 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; +import { AccessScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { crypto } from "@app/lib/crypto"; @@ -9,12 +10,11 @@ import { logger } from "@app/lib/logger"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; 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 { AuthMethod, AuthTokenType } from "../auth/auth-type"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { TUserDALFactory } from "./user-dal"; import { TListUserGroupsDTO, TUpdateUserEmailDTO, TUpdateUserMfaDTO } from "./user-types"; @@ -37,9 +37,8 @@ type TUserServiceFactoryDep = { >; groupProjectDAL: Pick; orgDAL: Pick; - orgMembershipDAL: Pick; + membershipUserDAL: Pick; tokenService: Pick; - projectMembershipDAL: Pick; smtpService: Pick; permissionService: TPermissionServiceFactory; userAliasDAL: Pick; @@ -50,8 +49,7 @@ export type TUserServiceFactory = ReturnType; export const userServiceFactory = ({ userDAL, orgDAL, - orgMembershipDAL, - projectMembershipDAL, + membershipUserDAL, groupProjectDAL, tokenService, smtpService, @@ -183,13 +181,19 @@ export const userServiceFactory = ({ }; const checkUserScimRestriction = async (userId: string, tx?: Knex) => { - const userOrgs = await orgMembershipDAL.find({ userId }, { tx }); + const userOrgs = await membershipUserDAL.find( + { + actorUserId: userId, + scope: AccessScope.Organization + }, + { tx } + ); if (userOrgs.length === 0) { return false; } - const orgIds = userOrgs.map((membership) => membership.orgId); + const orgIds = userOrgs.map((membership) => membership.scopeOrgId); const organizations = await orgDAL.find({ $in: { id: orgIds } }, { tx }); return organizations.some((org) => org.scimEnabled); @@ -347,6 +351,23 @@ export const userServiceFactory = ({ const deleteUser = async (userId: string) => { const user = await userDAL.deleteById(userId); + + try { + if (user?.email) { + // Send email to user to confirm account deletion + await smtpService.sendMail({ + template: SmtpTemplates.AccountDeletionConfirmation, + subjectLine: "Your Infisical account has been deleted", + recipients: [user.email], + substitutions: { + email: user.email + } + }); + } + } catch (error) { + logger.error(error, `Failed to send account deletion confirmation email to ${user.email}`); + } + return user; }; @@ -380,9 +401,10 @@ export const userServiceFactory = ({ }; const getUserProjectFavorites = async (userId: string, orgId: string) => { - const orgMembership = await orgMembershipDAL.findOne({ - userId, - orgId + const orgMembership = await membershipUserDAL.findOne({ + scope: AccessScope.Organization, + actorUserId: userId, + scopeOrgId: orgId }); if (!orgMembership) { @@ -395,9 +417,10 @@ export const userServiceFactory = ({ }; const updateUserProjectFavorites = async (userId: string, orgId: string, projectIds: string[]) => { - const orgMembership = await orgMembershipDAL.findOne({ - userId, - orgId + const orgMembership = await membershipUserDAL.findOne({ + scope: AccessScope.Organization, + actorUserId: userId, + scopeOrgId: orgId }); if (!orgMembership) { @@ -406,18 +429,20 @@ export const userServiceFactory = ({ }); } - const matchingUserProjectMemberships = await projectMembershipDAL.find({ - userId, + const matchingUserProjectMemberships = await membershipUserDAL.find({ + scope: AccessScope.Project, + scopeOrgId: orgId, + actorUserId: userId, $in: { - projectId: projectIds + scopeProjectId: projectIds } }); const memberProjectFavorites = matchingUserProjectMemberships.map( - (projectMembership) => projectMembership.projectId + (projectMembership) => projectMembership.scopeProjectId as string ); - const updatedOrgMembership = await orgMembershipDAL.updateById(orgMembership.id, { + const updatedOrgMembership = await membershipUserDAL.updateById(orgMembership.id, { projectFavorites: memberProjectFavorites }); diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx new file mode 100644 index 0000000000..dcd58cf32d --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create AWS Certificate Manager PKI Sync" +openapi: "POST /api/v1/pki/syncs/aws-certificate-manager" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx new file mode 100644 index 0000000000..73fed2cdb8 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete AWS Certificate Manager PKI Sync" +openapi: "DELETE /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx new file mode 100644 index 0000000000..9191bbde36 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get AWS Certificate Manager PKI Sync by ID" +openapi: "GET /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx new file mode 100644 index 0000000000..821ddbd616 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List AWS Certificate Manager PKI Syncs" +openapi: "GET /api/v1/pki/syncs/aws-certificate-manager" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx new file mode 100644 index 0000000000..5ea989f2ae --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Certificates from AWS Certificate Manager" +openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/remove-certificates" +--- diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx new file mode 100644 index 0000000000..b97b7a9ab7 --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Certificates to AWS Certificate Manager" +openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/sync" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx new file mode 100644 index 0000000000..9b7382ce8d --- /dev/null +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update AWS Certificate Manager PKI Sync" +openapi: "PATCH /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 210c5145c2..444f74b5de 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -221,6 +221,7 @@ ] }, "documentation/platform/sso/auth0-oidc", + "documentation/platform/sso/pingone-oidc", { "group": "General OIDC", "pages": [ @@ -247,6 +248,7 @@ "documentation/platform/scim/okta", "documentation/platform/scim/azure", "documentation/platform/scim/jumpcloud", + "documentation/platform/scim/pingone", "documentation/platform/scim/group-mappings" ] } @@ -723,6 +725,7 @@ { "group": "Syncs", "pages": [ + "documentation/platform/pki/certificate-syncs/aws-certificate-manager", "documentation/platform/pki/certificate-syncs/azure-key-vault" ] } @@ -2523,6 +2526,18 @@ "api-reference/endpoints/pki/syncs/list", "api-reference/endpoints/pki/syncs/get-by-id", "api-reference/endpoints/pki/syncs/options", + { + "group": "AWS Certificate Manager", + "pages": [ + "api-reference/endpoints/pki/syncs/aws-certificate-manager/list", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/create", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/update", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/delete", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates", + "api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates" + ] + }, { "group": "Azure Key Vault", "pages": [ diff --git a/docs/documentation/platform/gateways/gateway-deployment.mdx b/docs/documentation/platform/gateways/gateway-deployment.mdx index 40b98965a4..cef258c417 100644 --- a/docs/documentation/platform/gateways/gateway-deployment.mdx +++ b/docs/documentation/platform/gateways/gateway-deployment.mdx @@ -95,7 +95,7 @@ To successfully deploy an Infisical Gateway for use, follow these steps in order Ensure a relay server is running and accessible before you deploy any gateways. You have two options: - **Managed relay (Infisical Cloud, US/EU only):** Managed relays are only available for Infisical Cloud instances in the US and EU regions. If you are using Infisical Cloud in these regions, you can use the provided managed relay. - - **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the Relay Deployment Guide. + - **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the [Relay Deployment Guide](/documentation/platform/gateways/relay-deployment). Make sure the Infisical CLI is installed on the machine or environment where you plan to deploy the gateway. The CLI is required for gateway installation and management. diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index c04f2f7d3d..33945337b6 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -6,8 +6,8 @@ description: "How to access private network resources from Infisical" ![Architecture Overview](../../../images/platform/gateways/gateway-highlevel-diagram.png) -The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. -This is particularly useful when Infisical isn't hosted within the same network as the resources it needs to reach. +The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment. +This is particularly useful when Infisical isn't hosted within the same network as the resources it needs to reach. This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases. @@ -25,7 +25,7 @@ The Gateway system consists of two primary components working together to enable A Gateway is a lightweight service that you deploy within your own network infrastructure to provide secure access to your private resources. Think of it as a secure bridge between Infisical and your internal systems. - Gateways must be deployed within the same network where your target resources are located, with direct network connectivity to the private resources you want Infisical to access. + Gateways must be deployed within the same network where your target resources are located, with direct network connectivity to the private resources you want Infisical to access. For different networks, regions, or isolated environments, you'll need to deploy separate gateways. **Core Functions:** @@ -40,7 +40,7 @@ The Gateway system consists of two primary components working together to enable **Core Functions:** - **Traffic Routing**: Routes encrypted traffic between the Infisical platform and your gateways without storing or inspecting the data - - **Network Isolation**: Enables secure communication without requiring direct network connections between Infisical and your private infrastructure + - **Network Isolation**: Enables secure communication without requiring direct network connections between Infisical and your private infrastructure - **Authentication Management**: Validates SSH certificates and manages secure routing between authenticated gateways **Deployment Options:** @@ -59,6 +59,12 @@ The Gateway system uses SSH reverse tunnels for secure, firewall-friendly connec 3. **Request Routing**: When Infisical needs to access your resources, requests are routed through the relay server to the already-established gateway connection 4. **Resource Access**: The gateway receives the routed requests and connects to your private resources on behalf of Infisical +## Health Check + +To monitor their operational status, both gateways and relays transmit hourly heartbeats. A component is considered unhealthy if a heartbeat is not received for over an hour. + +Infisical automatically notifies all organization admins of unhealthy gateway or relay statuses through email and in-app notifications. + ## Getting Started Ready to set up your gateway? Follow the guides below. @@ -75,4 +81,4 @@ Ready to set up your gateway? Follow the guides below. Learn about the security model and implementation best practices. - \ No newline at end of file + diff --git a/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx b/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx new file mode 100644 index 0000000000..de064bf4e5 --- /dev/null +++ b/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx @@ -0,0 +1,146 @@ +--- +title: "AWS Certificate Manager" +description: "Learn how to configure an AWS Certificate Manager Certificate Sync for Infisical PKI." +--- + +**Prerequisites:** + +- Set up and configure a [Certificate Authority](/documentation/platform/pki/overview) +- Create an [AWS Connection](/integrations/app-connections/aws) + + + The AWS Certificate Manager Certificate Sync requires the following ACM permissions to be set on the IAM user/role + for Infisical to sync certificates to AWS Certificate Manager: `acm:ListCertificates`, `acm:DescribeCertificate`, `acm:ImportCertificate`, `acm:DeleteCertificate`, and `acm:ListTagsForCertificate`. + + These permissions allow Infisical to list, import, tag, and manage certificates in your AWS Certificate Manager service. + + + + Certificates synced to AWS Certificate Manager will be stored as imported certificates, preserving both the certificate and private key components. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Certificate Syncs** tab. Click on the **Add Sync** button. + ![Certificate Syncs Tab](/images/certificate-syncs/general/certificate-sync-tab.png) + + 2. Select the **AWS Certificate Manager** option. + ![Select ACM](/images/certificate-syncs/aws-certificate-manager/select-acm-option.png) + + 3. Configure the **Source** from where certificates should be retrieved, then click **Next**. + ![Configure Source](/images/certificate-syncs/aws-certificate-manager/acm-source.png) + + - **PKI Subscriber**: The PKI subscriber to retrieve certificates from. + + 4. Configure the **Destination** to where certificates should be deployed, then click **Next**. + ![Configure Destination](/images/certificate-syncs/aws-certificate-manager/acm-destination.png) + + - **AWS Connection**: The AWS Connection to authenticate with. + - **AWS Region**: The AWS region where certificates should be stored. + + 5. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**. + ![Configure Options](/images/certificate-syncs/aws-certificate-manager/acm-options.png) + + - **Auto-Sync Enabled**: If enabled, certificates will automatically be synced from the source PKI subscriber when changes occur. Disable to enforce manual syncing only. + - **Enable Certificate Removal**: If enabled, Infisical will remove expired certificates from the destination during sync operations. Disable this option if you intend to manage certificate cleanup manually. + - **Certificate Name Schema** (Optional): Customize how certificate tags are generated in AWS Certificate Manager. Must include `{{certificateId}}` as a placeholder for the certificate ID to ensure proper certificate identification and management. If not specified, defaults to `Infisical-{{certificateId}}`. + + + **AWS Certificate Manager Certificate Limits**: AWS Certificate Manager has limits on the number of certificates per account and region. Refer to AWS documentation for current limits. Deleted certificates count toward your quota until they are permanently purged by AWS (typically after 30 days). + + + 6. Configure the **Details** of your AWS Certificate Manager Certificate Sync, then click **Next**. + ![Configure Details](/images/certificate-syncs/aws-certificate-manager/acm-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your AWS Certificate Manager Certificate Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/certificate-syncs/aws-certificate-manager/acm-review.png) + + 8. If enabled, your AWS Certificate Manager Certificate Sync will begin syncing your certificates to the destination endpoint. + ![Sync Certificates](/images/certificate-syncs/aws-certificate-manager/acm-synced.png) + + + + To create an **AWS Certificate Manager Certificate Sync**, make an API request to the [Create AWS Certificate Manager Certificate Sync](/api-reference/endpoints/pki/syncs/aws-certificate-manager/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/pki/syncs/aws-certificate-manager \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-acm-cert-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example certificate sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "destination": "aws-certificate-manager", + "isAutoSyncEnabled": true, + "syncOptions": { + "canRemoveCertificates": true, + "certificateNameSchema": "myapp-{{certificateId}}" + }, + "destinationConfig": { + "region": "us-east-1" + } + }' + ``` + + ### Sample response + + ```json Response + { + "pkiSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-acm-cert-sync", + "description": "an example certificate sync", + "destination": "aws-certificate-manager", + "isAutoSyncEnabled": true, + "destinationConfig": { + "region": "us-east-1" + }, + "syncOptions": { + "canRemoveCertificates": true, + "certificateNameSchema": "myapp-{{certificateId}}" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z" + } + } + ``` + + + +## Certificate Management + +Your AWS Certificate Manager Certificate Sync will: + +- **Automatic Deployment**: Deploy new certificates issued by your PKI subscriber to AWS Certificate Manager +- **Certificate Updates**: Update certificates in AWS Certificate Manager when renewals occur +- **Expiration Handling**: Optionally remove expired certificates from AWS Certificate Manager (if enabled) +- **Tagging**: Automatically tag certificates with an InfisicalCertificate tag for easy identification and management + + + AWS Certificate Manager Certificate Syncs support both automatic and manual synchronization modes. When auto-sync is enabled, certificates are automatically deployed as they are issued or renewed. + + +## Manual Certificate Sync + +You can manually trigger certificate synchronization from your PKI subscriber to AWS Certificate Manager using the sync certificates functionality. This is useful for: + +- Initial setup when you have existing certificates to deploy +- One-time sync of specific certificates +- Testing certificate sync configurations +- Force sync after making changes + +To manually sync certificates, use the [Sync Certificates](/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates) API endpoint or the manual sync option in the Infisical UI. + + +AWS Certificate Manager does not support importing certificates back into Infisical due to security limitations where private keys cannot be extracted from AWS Certificate Manager. Only certificates imported into ACM (not AWS-issued certificates) can be managed by the sync. + \ No newline at end of file diff --git a/docs/documentation/platform/scim/overview.mdx b/docs/documentation/platform/scim/overview.mdx index 5b85079610..21f06cd8d8 100644 --- a/docs/documentation/platform/scim/overview.mdx +++ b/docs/documentation/platform/scim/overview.mdx @@ -24,3 +24,4 @@ SCIM providers: - [Okta SCIM](/documentation/platform/scim/okta) - [Azure SCIM](/documentation/platform/scim/azure) - [JumpCloud SCIM](/documentation/platform/scim/jumpcloud) +- [PingOne SCIM](/documentation/platform/scim/pingone) diff --git a/docs/documentation/platform/scim/pingone.mdx b/docs/documentation/platform/scim/pingone.mdx new file mode 100644 index 0000000000..b7263290db --- /dev/null +++ b/docs/documentation/platform/scim/pingone.mdx @@ -0,0 +1,149 @@ +--- +title: "PingOne SCIM" +description: "Learn how to configure SCIM provisioning with PingOne for Infisical." +--- + + + PingOne SCIM provisioning is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase a self-hosted license to use it. + + +Prerequisites: +- [Configure PingOne OIDC for Infisical](/documentation/platform/sso/pingone-oidc) + + + + In Infisical, head to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. Under SCIM Configuration, + press the **Enable SCIM provisioning** toggle to allow PingOne to provision/deprovision users for your organization. + + ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) + + Next, press **Manage SCIM Tokens** and then **Create** to generate a SCIM token for PingOne. + + ![SCIM create token](/images/platform/scim/scim-create-token.png) + + Next, copy the **SCIM URL** and **New SCIM Token** to use when configuring SCIM in PingOne. + + ![SCIM copy token](/images/platform/scim/scim-copy-token.png) + + + Inside your PingOne environment, navigate to Directory > Users. Add any users and/or groups to your application that you would like + to be provisioned over to Infisical. + + ![SCIM PingOne Users and Groups](/images/platform/scim/pingone/pingone-create-user.png) + + + + + **1. Create a new connection** + + In PingOne, head to Integrations > Provisioning, and inside provisioning, press the **Connections** tab. Here you'll see a plus icon to add a new connection. + + ![SCIM PingOne Connections](/images/platform/scim/pingone/pingone-new-connection.png) + + ![SCIM PingOne Connections](/images/platform/scim/pingone/pingone-new-connection-identity-store.png) + + Select the "Identity Store" option. + + + ![SCIM PingOne SCIM Outbound](/images/platform/scim/pingone/pingone-connection-select-scim-outbound.png) + + Search for the "SCIM Outbound" option to start the configuration process for SCIM. Finally, press the **Next** button. Give the connection a name and optionally add a description. + + **2. Configure the connection** + + Once you have selected the SCIM Outbound option, you'll be prompted to enter the authentication details that PingOne will use to authenticate with Infisical SCIM. This is the **SCIM URL** and **New SCIM Token** from the previous step. + + ![SCIM PingOne SCIM Outbound](/images/platform/scim/pingone/pingone-connection-auth.png) + + Set the following fields: + - `SCIM BASE URL`: Input the **SCIM URL** from the previous step. + - `Users Resource`: Leave as default, `/Users`. + - `Groups Resource`: Leave as default, `/Groups`. + - `SCIM Version`: Leave as default, `2.0`. + - `Authentication Method`: Select `OAuth 2 Bearer Token`. + - `Oauth Access Token`: Input the **New SCIM Token** from step 1. + - `Auth Type Header`: Select `Bearer`. + + Once this is done, you can press the **Test Connection** button to check that SCIM is configured properly. You should see a success message saying "Connection Successful". + If the connection is successful, press the "Next" button. + + In the final step, you'll be prompted to configure the mappings for the connection. + + Set the following fields: + - `User Filter Expression`: `email.value Eq "%s"`. + - `User Identifier`: `workEmail`. + - `Deprovision on Rule Deletion:` Enabled. + + ![SCIM PingOne Connection Mappings](/images/platform/scim/pingone/pingone-connection-preferences.png) + + Once this is configured, press the "Save" button. + + **3. Enable the connection** + + Finally, remember to enable the connection by pressing the enable toggle. + + ![SCIM PingOne Connection Enable](/images/platform/scim/pingone/pingone-connection-enable.png) + + + + + **1. Create a new rule** + + After creating a connection, you can now access the "Rules" tab under the Provisioning section. Here you can configure the rules for the connection. + + ![SCIM PingOne Create New Rule 1](/images/platform/scim/pingone/pingone-new-rule-1.png) + + ![SCIM PingOne Create New Rule 2](/images/platform/scim/pingone/pingone-new-rule-2.png) + + Select the "New Rule" button and choose a name for the rule, then press the "Create Rule" button. + + **2. Configure the rule connection** + + Once you have created a rule, you now need to configure the connection to use for the rule. + + ![SCIM PingOne Create New Rule 3](/images/platform/scim/pingone/pingone-rule-select-connection.png) + + Select the connection you created in the previous step and press the "Save" button. + + **3. Configure the rule user filter** + + ![SCIM PingOne Create New Rule 4](/images/platform/scim/pingone/pingone-rule-select-user-filter.png) + + Select the Edit pencil icon to open the user filter configuration. This step dictates which users will be provisioned to Infisical. + + ![SCIM PingOne Create New Rule 5](/images/platform/scim/pingone/pingone-rule-user-filter.png) + + In this case, we are provisioning all users that are enabled in PingOne. Configure your user filter to match your desired users, and then press the "Save" button. + + **4. Configure Groups** + + This step is optional and only relevant if you want to provision PingOne groups to Infisical. + + ![SCIM PingOne Create New Rule 6](/images/platform/scim/pingone/pingone-rule-group-provisioning-tab.png) + + Open the "Group Provisioning" tab and press the "Add Groups" button to select which groups will be provisioned to Infisical. + + ![SCIM PingOne Create New Rule 7](/images/platform/scim/pingone/pingone-select-group.png) + + Select the groups you want to provision to Infisical and press the "Save" button. + + **5. Enable the rule** + + Once you have configured the rule, you can enable it by pressing the "Enable" toggle. + + ![SCIM PingOne Create New Rule 8](/images/platform/scim/pingone/pingone-rule-enable.png) + + + +**FAQ** + + + + Infisical's SCIM implementation accounts for retaining the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps in the platform. + + For this reason, SCIM-provisioned users are initialized but must finish setting up their account when logging in the first time by creating a master encryption/decryption key. With this implementation, IdPs and SCIM providers cannot and will not have access to the decryption key needed to decrypt your secrets. + + diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index 4b54c053d7..0f616c5196 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -6,7 +6,7 @@ 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 + should contact sales@infisical.com to purchase a self-hosted license to use it. @@ -55,7 +55,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." Enabling OIDC allows members in your organization to log into Infisical via Auth0. - ![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png) + ![OIDC auth0 enable OIDC](../../../images/sso/enable-oidc.png) diff --git a/docs/documentation/platform/sso/general-oidc/overview.mdx b/docs/documentation/platform/sso/general-oidc/overview.mdx index 07ddaaedd5..586f66f26a 100644 --- a/docs/documentation/platform/sso/general-oidc/overview.mdx +++ b/docs/documentation/platform/sso/general-oidc/overview.mdx @@ -7,7 +7,7 @@ description: "Learn how to configure OIDC for Infisical SSO with any OIDC-compli 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 + should contact sales@infisical.com to purchase a self-hosted license to use it. diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx index 2c75fc6fe3..727bf9be69 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -7,7 +7,7 @@ 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 + you should contact sales@infisical.com to purchase a self-hosted license to use it. @@ -82,7 +82,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." 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) + ![OIDC keycloak enable OIDC](/images/sso/enable-oidc.png) diff --git a/docs/documentation/platform/sso/pingone-oidc.mdx b/docs/documentation/platform/sso/pingone-oidc.mdx new file mode 100644 index 0000000000..1fea5f8154 --- /dev/null +++ b/docs/documentation/platform/sso/pingone-oidc.mdx @@ -0,0 +1,108 @@ +--- +title: "PingOne OIDC" +description: "Learn how to configure PingOne OIDC for Infisical SSO." +--- + + + PingOne 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 a self-hosted license to use + it. + + + + + 1.1. From the Application's Page, create a new OIDC Web App application. + ![OIDC pingone create application](../../../images/sso/pingone-oidc/pingone-create-application.png) + + 1.2. Enable the application by pressing the "Enable" toggle. + ![OIDC PingOne Enable Application](../../../images/sso/pingone-oidc/pingone-enable-application.png) + + + 1.3. In the Application "Configuration" tab, press the "Edit" pencil icon to configure the application callback URI. + ![OIDC PingOne Edit Application Configuration](../../../images/sso/pingone-oidc/pingone-edit-application-configuration.png) + + + 1.4 Set the Redirect URL to `https://app.infisical.com/api/v1/sso/oidc/callback` and press the "Save" button. + ![OIDC PingOne Edit Redirect URI](../../../images/sso/pingone-oidc/pingone-edit-application-redirect-uri.png) + + + + If you're self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain. + + + + 1.5 After configuring the redirect URL, go to the "Attribute Mappings" tab and press the "Edit" pencil icon to configure the attribute mappings. + ![OIDC PingOne Edit Attribute Mappings](../../../images/sso/pingone-oidc/pingone-edit-application-attribute-mappings.png) + + 1.6 Map the following attributes: + - `email` -> `Email Address` + - `name` -> `Username` + ![OIDC PingOne Edit Attribute Mappings](../../../images/sso/pingone-oidc/pingone-edit-application-attribute-mappings-2.png) + + Once done, press the "Save" button. + + + + 2.1. Open the "Overview" tab and copy the **Client ID** and **Client Secret**. + ![OIDC PingOne Application Credential](../../../images/sso/pingone-oidc/pingone-overview-credentials.png) + + 2.2. Still in the "Overview" tab, scroll down to the Connection Details section and retrieve the **OIDC Discovery Endpoint**. + ![OIDC PingOne OIDC Discovery Endpoint](../../../images/sso/pingone-oidc/pingone-overview-oidc-discovery-endpoint.png) + + Keep these values handy as we will need them in the next steps. + + + + 3.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **OIDC**. + ![OIDC SSO Connect](../../../images/sso/connect-oidc.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 PingOne paste values into Infisical](../../../images/sso/pingone-oidc/infisical-configure-oidc.png) + + + Currently, the following JWT signature algorithms are supported: RS256, RS512, HS256, and EdDSA + + + Once you've done that, press **Update** to complete the required configuration. + + + + Enabling OIDC allows members in your organization to log into Infisical via PingOne + + ![OIDC PingOne enable OIDC](../../../images/sso/enable-oidc.png) + + + + Enforcing OIDC SSO ensures that members in your organization can only access Infisical + by logging into the organization via PingOne. + + To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one PingOne user with Infisical. + Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO. + + + We recommend ensuring that your account is provisioned using the application in PingOne + prior to enforcing OIDC SSO to prevent any unintended issues. + + + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + + + + + + If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login. + + + + 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 absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) + diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-destination.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-destination.png new file mode 100644 index 0000000000..42a20fc996 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-destination.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-details.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-details.png new file mode 100644 index 0000000000..483cee003d Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-details.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-options.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-options.png new file mode 100644 index 0000000000..aa08b2d193 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-options.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-review.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-review.png new file mode 100644 index 0000000000..5f7b216ad2 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-review.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-source.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-source.png new file mode 100644 index 0000000000..0d92fe69e1 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-source.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/acm-synced.png b/docs/images/certificate-syncs/aws-certificate-manager/acm-synced.png new file mode 100644 index 0000000000..7e1ed12c52 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/acm-synced.png differ diff --git a/docs/images/certificate-syncs/aws-certificate-manager/select-acm-option.png b/docs/images/certificate-syncs/aws-certificate-manager/select-acm-option.png new file mode 100644 index 0000000000..79515516a4 Binary files /dev/null and b/docs/images/certificate-syncs/aws-certificate-manager/select-acm-option.png differ diff --git a/docs/images/platform/scim/pingone/pingone-connection-auth.png b/docs/images/platform/scim/pingone/pingone-connection-auth.png new file mode 100644 index 0000000000..c29d8b7024 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-connection-auth.png differ diff --git a/docs/images/platform/scim/pingone/pingone-connection-enable.png b/docs/images/platform/scim/pingone/pingone-connection-enable.png new file mode 100644 index 0000000000..24e4237429 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-connection-enable.png differ diff --git a/docs/images/platform/scim/pingone/pingone-connection-preferences.png b/docs/images/platform/scim/pingone/pingone-connection-preferences.png new file mode 100644 index 0000000000..e8372fd68b Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-connection-preferences.png differ diff --git a/docs/images/platform/scim/pingone/pingone-connection-select-scim-outbound.png b/docs/images/platform/scim/pingone/pingone-connection-select-scim-outbound.png new file mode 100644 index 0000000000..a83d4a590c Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-connection-select-scim-outbound.png differ diff --git a/docs/images/platform/scim/pingone/pingone-create-user.png b/docs/images/platform/scim/pingone/pingone-create-user.png new file mode 100644 index 0000000000..f48d4e74c6 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-create-user.png differ diff --git a/docs/images/platform/scim/pingone/pingone-new-connection-identity-store.png b/docs/images/platform/scim/pingone/pingone-new-connection-identity-store.png new file mode 100644 index 0000000000..371645acee Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-new-connection-identity-store.png differ diff --git a/docs/images/platform/scim/pingone/pingone-new-connection.png b/docs/images/platform/scim/pingone/pingone-new-connection.png new file mode 100644 index 0000000000..ea17e15f4d Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-new-connection.png differ diff --git a/docs/images/platform/scim/pingone/pingone-new-rule-1.png b/docs/images/platform/scim/pingone/pingone-new-rule-1.png new file mode 100644 index 0000000000..065ffb446d Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-new-rule-1.png differ diff --git a/docs/images/platform/scim/pingone/pingone-new-rule-2.png b/docs/images/platform/scim/pingone/pingone-new-rule-2.png new file mode 100644 index 0000000000..4aedebaad3 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-new-rule-2.png differ diff --git a/docs/images/platform/scim/pingone/pingone-rule-enable.png b/docs/images/platform/scim/pingone/pingone-rule-enable.png new file mode 100644 index 0000000000..0cc3f58a10 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-rule-enable.png differ diff --git a/docs/images/platform/scim/pingone/pingone-rule-group-provisioning-tab.png b/docs/images/platform/scim/pingone/pingone-rule-group-provisioning-tab.png new file mode 100644 index 0000000000..25decadba8 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-rule-group-provisioning-tab.png differ diff --git a/docs/images/platform/scim/pingone/pingone-rule-select-connection.png b/docs/images/platform/scim/pingone/pingone-rule-select-connection.png new file mode 100644 index 0000000000..5eabc22f37 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-rule-select-connection.png differ diff --git a/docs/images/platform/scim/pingone/pingone-rule-select-user-filter.png b/docs/images/platform/scim/pingone/pingone-rule-select-user-filter.png new file mode 100644 index 0000000000..d40a951f0d Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-rule-select-user-filter.png differ diff --git a/docs/images/platform/scim/pingone/pingone-rule-user-filter.png b/docs/images/platform/scim/pingone/pingone-rule-user-filter.png new file mode 100644 index 0000000000..3470f56781 Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-rule-user-filter.png differ diff --git a/docs/images/platform/scim/pingone/pingone-select-group.png b/docs/images/platform/scim/pingone/pingone-select-group.png new file mode 100644 index 0000000000..4a234b3b1b Binary files /dev/null and b/docs/images/platform/scim/pingone/pingone-select-group.png differ diff --git a/docs/images/sso/auth0-oidc/enable-oidc.png b/docs/images/sso/enable-oidc.png similarity index 100% rename from docs/images/sso/auth0-oidc/enable-oidc.png rename to docs/images/sso/enable-oidc.png diff --git a/docs/images/sso/keycloak-oidc/enable-oidc.png b/docs/images/sso/keycloak-oidc/enable-oidc.png deleted file mode 100644 index 0a43f22ede..0000000000 Binary files a/docs/images/sso/keycloak-oidc/enable-oidc.png and /dev/null differ diff --git a/docs/images/sso/pingone-oidc/infisical-configure-oidc.png b/docs/images/sso/pingone-oidc/infisical-configure-oidc.png new file mode 100644 index 0000000000..b60ff0f270 Binary files /dev/null and b/docs/images/sso/pingone-oidc/infisical-configure-oidc.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-create-application.png b/docs/images/sso/pingone-oidc/pingone-create-application.png new file mode 100644 index 0000000000..7f188da7d5 Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-create-application.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-edit-application-attribute-mappings-2.png b/docs/images/sso/pingone-oidc/pingone-edit-application-attribute-mappings-2.png new file mode 100644 index 0000000000..05f212b73f Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-edit-application-attribute-mappings-2.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-edit-application-attribute-mappings.png b/docs/images/sso/pingone-oidc/pingone-edit-application-attribute-mappings.png new file mode 100644 index 0000000000..1beba0117b Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-edit-application-attribute-mappings.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-edit-application-configuration.png b/docs/images/sso/pingone-oidc/pingone-edit-application-configuration.png new file mode 100644 index 0000000000..9be619298c Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-edit-application-configuration.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-edit-application-redirect-uri.png b/docs/images/sso/pingone-oidc/pingone-edit-application-redirect-uri.png new file mode 100644 index 0000000000..e5292a85f8 Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-edit-application-redirect-uri.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-enable-application.png b/docs/images/sso/pingone-oidc/pingone-enable-application.png new file mode 100644 index 0000000000..51219b9fda Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-enable-application.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-overview-credentials.png b/docs/images/sso/pingone-oidc/pingone-overview-credentials.png new file mode 100644 index 0000000000..af609ff0bb Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-overview-credentials.png differ diff --git a/docs/images/sso/pingone-oidc/pingone-overview-oidc-discovery-endpoint.png b/docs/images/sso/pingone-oidc/pingone-overview-oidc-discovery-endpoint.png new file mode 100644 index 0000000000..da7e981b6e Binary files /dev/null and b/docs/images/sso/pingone-oidc/pingone-overview-oidc-discovery-endpoint.png differ diff --git a/docs/integrations/app-connections/aws.mdx b/docs/integrations/app-connections/aws.mdx index 0d18856d2f..fc9f6722ef 100644 --- a/docs/integrations/app-connections/aws.mdx +++ b/docs/integrations/app-connections/aws.mdx @@ -177,6 +177,45 @@ Infisical supports two methods for connecting to AWS. + + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync certificates to AWS Certificate Manager: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowCertificateManagerAccess", + "Effect": "Allow", + "Action": [ + "acm:ListCertificates", + "acm:DescribeCertificate", + "acm:GetCertificate", + "acm:ImportCertificate", + "acm:ExportCertificate", + "acm:DeleteCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate", + "acm:ListTagsForCertificate" + ], + "Resource": "*" + } + ] + } + ``` + + - **ListCertificates**: Lists all certificates in the account + - **ImportCertificate**: Imports certificates from Infisical into AWS Certificate Manager + - **ExportCertificate**: Exports certificates for synchronization + - **DeleteCertificate**: Removes certificates that are no longer managed by Infisical + - **DescribeCertificate** and **GetCertificate**: Retrieves certificate details for comparison during sync + - Tag-related permissions: Manages certificate tags for identification and organization + + + + @@ -354,6 +393,45 @@ Infisical supports two methods for connecting to AWS. + + + + Use the following custom policy to grant the minimum permissions required by Infisical to sync certificates to AWS Certificate Manager: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowCertificateManagerAccess", + "Effect": "Allow", + "Action": [ + "acm:ListCertificates", + "acm:DescribeCertificate", + "acm:GetCertificate", + "acm:ImportCertificate", + "acm:ExportCertificate", + "acm:DeleteCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate", + "acm:ListTagsForCertificate" + ], + "Resource": "*" + } + ] + } + ``` + + - **ListCertificates**: Lists all certificates in the account + - **ImportCertificate**: Imports certificates from Infisical into AWS Certificate Manager + - **ExportCertificate**: Exports certificates for synchronization + - **DeleteCertificate**: Removes certificates that are no longer managed by Infisical + - **DescribeCertificate** and **GetCertificate**: Retrieves certificate details for comparison during sync + - Tag-related permissions: Manages certificate tags for identification and organization + + + + diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx index 7ead4d3ddd..f6ddaad582 100644 --- a/docs/sdks/languages/java.mdx +++ b/docs/sdks/languages/java.mdx @@ -105,6 +105,22 @@ sdk.Auth().UniversalAuthLogin( - `clientId` (string): The client ID of your Machine Identity. - `clientSecret` (string): The client secret of your Machine Identity. +### AWS Auth + +```java +public void AwsAuthLogin( + String identityId +) +throws InfisicalException +``` + +```java +sdk.Auth().AwsAuthLogin(""); +``` + +**Parameters:** +- `identityId` (String): The ID of the machine identity to authenticate with. + ### LDAP Auth ```java diff --git a/docs/self-hosting/deployment-options/native/linux-package/installation.mdx b/docs/self-hosting/deployment-options/native/linux-package/installation.mdx index 30ef832426..2a799cb6b7 100644 --- a/docs/self-hosting/deployment-options/native/linux-package/installation.mdx +++ b/docs/self-hosting/deployment-options/native/linux-package/installation.mdx @@ -25,35 +25,46 @@ Please ensure you have the following before beginning installation of Infisical: Select your Linux distribution to get started. Only AMD64-based systems are supported at this time, ARM support is coming soon. + - - Add the Infisical repository: - ```bash - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.deb.sh' | sudo -E bash - ``` + + As of October 10, 2025, all future releases for Debian/Ubuntu will be distributed via the official Infisical repository at https://artifacts-infisical-core.infisical.com. + No new releases will be published for Debian/Ubuntu on Cloudsmith going forward. + - Install Infisical: - ```bash - sudo apt-get update && sudo apt-get install -y infisical-core - ``` + Add the Infisical repository: + ```bash + curl -1sLf 'https://artifacts-infisical-core.infisical.com/setup.deb.sh' | sudo -E bash + ``` + + Install Infisical: + ```bash + sudo apt-get update && sudo apt-get install -y infisical-core + ``` + + > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://github.com/Infisical/infisical/releases). All versions from `infisical-core-0.150.0~nightly~20251005` and above are supported. - > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). + - - Add the Infisical repository: - ```bash - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/setup.rpm.sh' | sudo -E bash - ``` + + As of October 10, 2025, all future releases for Red Hat/CentOS/Amazon Linux will be distributed via the official Infisical repository at https://artifacts-infisical-core.infisical.com. + No new releases will be published for Red Hat/CentOS/Amazon Linux on Cloudsmith going forward. + - Install Infisical: - ```bash - sudo yum install infisical-core - ``` + Add the Infisical repository: + ```bash + curl -1sLf 'https://artifacts-infisical-core.infisical.com/setup.rpm.sh' | sudo -E bash + ``` - > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://cloudsmith.io/~infisical/repos/infisical-core/packages/). - + Install Infisical: + ```bash + sudo yum install infisical-core + ``` + > **Note**: For production use, we recommend locking to a specific version to ensure consistency. [View available versions](https://github.com/Infisical/infisical/releases). All versions from `infisical-core-0.150.0~nightly~20251005` and above are supported. + + Verify the installation: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3c73d9fe3a..a9f4b070ce 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -55,7 +55,7 @@ "@ucast/mongo2js": "^1.3.4", "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", @@ -65,7 +65,7 @@ "i18next": "^24.1.0", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", - "jspdf": "^2.5.2", + "jspdf": "^3.0.2", "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", "lexical": "^0.29.0", @@ -99,6 +99,7 @@ "@eslint/js": "^9.15.0", "@kesills/eslint-config-airbnb-typescript": "^20.0.0", "@stylistic/eslint-plugin": "^2.12.1", + "@tailwindcss/postcss": "^4.1.14", "@tailwindcss/typography": "^0.5.15", "@tanstack/eslint-plugin-router": "^1.87.6", "@tanstack/router-devtools": "^1.87.9", @@ -113,7 +114,6 @@ "@types/react-dom": "^18.3.1", "@types/react-helmet": "^6.1.11", "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.20", "eslint": "^8.57.1", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.1.0", @@ -126,8 +126,8 @@ "globals": "^15.12.0", "postcss": "^8.4.49", "prettier": "3.4.2", - "prettier-plugin-tailwindcss": "^0.6.9", - "tailwindcss": "^3.4.16", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.1.14", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^5.4.18", @@ -1454,51 +1454,17 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "minipass": "^7.0.4" }, "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1515,6 +1481,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1534,9 +1511,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2193,17 +2170,6 @@ "tsyringe": "^4.8.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -3903,6 +3869,282 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", + "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "postcss": "^8.4.41", + "tailwindcss": "4.1.14" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", @@ -4387,6 +4629,12 @@ "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4906,13 +5154,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -4940,13 +5181,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, "node_modules/argon2-browser": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz", @@ -5204,61 +5438,10 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -5282,9 +5465,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -5626,18 +5809,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -5681,7 +5852,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -5697,9 +5867,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5710,14 +5880,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.2.tgz", - "integrity": "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "get-intrinsic": "^1.2.5" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -5744,16 +5913,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001688", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", @@ -5902,6 +6061,16 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/cipher-base": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", @@ -6026,16 +6195,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6085,7 +6244,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -6499,7 +6657,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -6559,6 +6716,16 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -6577,13 +6744,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -6609,13 +6769,6 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6661,12 +6814,12 @@ } }, "node_modules/dunder-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, @@ -6674,13 +6827,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.73", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", @@ -6716,12 +6862,13 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -6860,9 +7007,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7709,6 +7856,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -7826,29 +7990,11 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.3" } }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -7865,20 +8011,6 @@ "node": ">= 6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/framer-motion": { "version": "11.14.1", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.14.1.tgz", @@ -7986,21 +8118,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8018,6 +8150,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -8181,7 +8326,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -8234,19 +8378,62 @@ } }, "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", "license": "MIT", "dependencies": { "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=4" + "node": ">= 0.8" } }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hash-base/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==", + "license": "MIT", + "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/hash-base/node_modules/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==", + "license": "MIT" + }, + "node_modules/hash-base/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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/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==", + "license": "MIT" + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -8628,6 +8815,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -8775,7 +8968,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9090,13 +9282,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -9152,7 +9343,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -9202,30 +9392,14 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { - "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -9306,29 +9480,22 @@ } }, "node_modules/jspdf": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", - "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", + "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.2", - "atob": "^2.1.2", - "btoa": "^1.2.1", + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { - "canvg": "^3.0.6", + "canvg": "^3.0.11", "core-js": "^3.6.0", - "dompurify": "^2.5.4", + "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, - "node_modules/jspdf/node_modules/dompurify": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", - "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "optional": true - }, "node_modules/jsrp": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/jsrp/-/jsrp-0.2.4.tgz", @@ -9440,17 +9607,243 @@ "url": "https://github.com/sponsors/dmonad" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lines-and-columns": { @@ -9517,27 +9910,20 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/magic-string": { - "version": "0.30.15", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", - "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10252,6 +10638,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/motion-dom": { "version": "11.14.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.1.tgz", @@ -10270,18 +10669,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -10391,16 +10778,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nprogress": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", @@ -10416,16 +10793,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -10628,13 +10995,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -10780,23 +11140,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10807,20 +11150,21 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "license": "MIT", "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=0.12" + "node": ">= 0.10" } }, "node_modules/performance-now": { @@ -10848,26 +11192,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/pkg-dir": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", @@ -10894,7 +11218,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10929,127 +11252,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/posthog-js": { "version": "1.198.0", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.198.0.tgz", @@ -11118,9 +11320,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz", - "integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==", + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", "dev": true, "license": "MIT", "engines": { @@ -11128,10 +11330,12 @@ }, "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", - "@zackad/prettier-plugin-twig-melody": "*", + "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", @@ -11149,6 +11353,12 @@ "@ianvs/prettier-plugin-sort-imports": { "optional": true }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, "@prettier/plugin-pug": { "optional": true }, @@ -11158,7 +11368,7 @@ "@trivago/prettier-plugin-sort-imports": { "optional": true }, - "@zackad/prettier-plugin-twig-melody": { + "@zackad/prettier-plugin-twig": { "optional": true }, "prettier-plugin-astro": { @@ -11219,7 +11429,6 @@ "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, "license": "MIT" }, "node_modules/prop-types": { @@ -11735,20 +11944,11 @@ "react-dom": ">=16.6.0" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, "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, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -11980,13 +12180,16 @@ } }, "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "license": "MIT", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/rollup": { @@ -12142,7 +12345,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -12180,16 +12382,23 @@ "license": "MIT" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -12291,19 +12500,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -12377,81 +12573,12 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "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, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/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, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -12592,20 +12719,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "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, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12643,76 +12756,6 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12776,42 +12819,11 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "license": "MIT" }, "node_modules/tapable": { "version": "2.2.1", @@ -12823,6 +12835,33 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -12840,29 +12879,6 @@ "dev": true, "license": "MIT" }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -12888,6 +12904,20 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.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", @@ -12938,13 +12968,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/tsconfck": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", @@ -13501,15 +13524,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -14219,7 +14241,6 @@ "version": "1.1.16", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz", "integrity": "sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -14245,107 +14266,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "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, - "license": "MIT", - "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/wrap-ansi-cjs/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, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/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, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1bc55c1c79..d890256500 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,9 @@ "lint:fix": "eslint --fix ./src", "type:check": "tsc --noEmit --project ./tsconfig.app.json" }, + "overrides": { + "sha.js": "2.4.12" + }, "dependencies": { "@casl/ability": "^6.7.2", "@casl/react": "^4.0.0", @@ -59,7 +62,7 @@ "@ucast/mongo2js": "^1.3.4", "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", @@ -69,7 +72,7 @@ "i18next": "^24.1.0", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", - "jspdf": "^2.5.2", + "jspdf": "^3.0.2", "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", "lexical": "^0.29.0", @@ -103,6 +106,7 @@ "@eslint/js": "^9.15.0", "@kesills/eslint-config-airbnb-typescript": "^20.0.0", "@stylistic/eslint-plugin": "^2.12.1", + "@tailwindcss/postcss": "^4.1.14", "@tailwindcss/typography": "^0.5.15", "@tanstack/eslint-plugin-router": "^1.87.6", "@tanstack/router-devtools": "^1.87.9", @@ -117,7 +121,6 @@ "@types/react-dom": "^18.3.1", "@types/react-helmet": "^6.1.11", "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.20", "eslint": "^8.57.1", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.1.0", @@ -130,8 +133,8 @@ "globals": "^15.12.0", "postcss": "^8.4.49", "prettier": "3.4.2", - "prettier-plugin-tailwindcss": "^0.6.9", - "tailwindcss": "^3.4.16", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.1.14", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^5.4.18", diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b7f1..a7f73a2d1d 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, } diff --git a/frontend/public/images/integrations/IBM.png b/frontend/public/images/integrations/IBM.png new file mode 100644 index 0000000000..7fafbb6c3b Binary files /dev/null and b/frontend/public/images/integrations/IBM.png differ diff --git a/frontend/public/images/integrations/RDP.png b/frontend/public/images/integrations/RDP.png new file mode 100644 index 0000000000..1336888b7e Binary files /dev/null and b/frontend/public/images/integrations/RDP.png differ diff --git a/frontend/public/images/integrations/SSH.png b/frontend/public/images/integrations/SSH.png new file mode 100644 index 0000000000..ff7708f9fd Binary files /dev/null and b/frontend/public/images/integrations/SSH.png differ diff --git a/frontend/src/components/app-connections/AppConnectionOption.tsx b/frontend/src/components/app-connections/AppConnectionOption.tsx index 2978dc9f07..30a37425c5 100644 --- a/frontend/src/components/app-connections/AppConnectionOption.tsx +++ b/frontend/src/components/app-connections/AppConnectionOption.tsx @@ -26,8 +26,8 @@ export const AppConnectionOption = ({

{children}

{!props.data.projectId && ( -
- +
+ Organization diff --git a/frontend/src/components/auth/CodeInputStep.tsx b/frontend/src/components/auth/CodeInputStep.tsx index f992c8da6d..297a9f8832 100644 --- a/frontend/src/components/auth/CodeInputStep.tsx +++ b/frontend/src/components/auth/CodeInputStep.tsx @@ -91,7 +91,7 @@ export default function CodeInputStep({ return (

{t("signup.step2-message")}

-

{email}

+

{email}

@@ -111,11 +111,11 @@ export default function CodeInputStep({ fields={6} onChange={setCode} {...propsPhone} - className="mb-2 mt-2" + className="mt-2 mb-2" />
{codeError && } -
+
-
+